클래스

자바에서 클래스는 객체를 정의하기 위한 설계도입니다. 주로 객체의 behavior나 properties를 정의하게 됩니다. 자바에서 사용하는 클래스는 크게 built-in classUser-Defined class로 나눌 수 있습니다. 이번 글에서는 User-Defined class에 대해 다루겠습니다.


클래스 구조 및 구성요소

기본적인 구조는 다음과 같습니다.

public class Integer extends Number implements Serializable, Comparable {
	// class members go here
}

Field

객체의 properties나 attribute를 정의하기 위해 사용합니다.

public class Person{
	//<modifiers> <data type> <field name> = <initial value>
	public int var = 10;
}

Method

객체의 behavior를 정의하기 위해 사용합니다. 아래서 자세히 다루겠습니다.

Blocks

Block은 {} 로 감싸진 하나이상의 Statements를 의미합니다. Block의 종류는 static blockinstance block이 있습니다.

1. static block

자바에서 static block은 클래스 로딩 시점에 단 한번만 실행됩니다. class는 여러개의 static block을 가질 수 있습니다.

neccessity of static block
constructor가 이를 대체할 수는 없을까요? constructor는 클래스의 인스턴스가 생성될 때마다 초기화를 합니다. 즉 클래스의 초기화에 사용되는 static block을 constructor가 대체하기는 어렵습니다.

아래와 같은 static final 변수가 있다고 가정하겠습니다.

public static final Map<String, String> initials = new HashMap<String, String>();

해당 변수를 한번만 초기화하기 위해서 in-line declaration을 사용하는 것도 가능하지만 복잡한 초기화를 해야할 경우는 static block을 활용할 수 있습니다.

public static final Map<String, String> initials = new HashMap<String ,String>();
static{
	initials.put("NAME","DEVBELLY");
}

더 안전하게 초기화하기 위해서는 아래와 같이 작성할 수 있습니다.

public static final Map<String,String> initials;
static{
	Map<String, String> map = new HashMap<String, String>();
	initials.put("NAME","DEVBELLY");
	initials = Collections.unmodifiableMap(map);
}

in-line 방식에서는 값을 채울 수 없기에 변수 initials를 unmodifiable map으로 초기화할 수 없습니다. 또한 constructor에서도 modifying method(put)을 사용한다면 예외가 발생하므로 사용할 수 없습니다.

사실 위에서 작성한 코드는 private static method 방식으로 대체할 수 있습니다.

public static final Map<String, String> initials = makeInitials();

private static Map<String, String> makeInitials() {
    Map<String, String> map = new HashMap<String, String>()
    map.put("NAME","DEVBELLY");
    // etc.
    return Collections.unmodifiableMap(map);
}

그렇다면 static block을 꼭 사용해야하는 경우는 어떤 것이 있을까요? 여러 다른 클래스를 단 한번만 초기화해야하는 경우, 특히 DI를 사용하는 경우입니다.

public class Coordinator {
    static {
        WorkerClass1.init();
        WorkerClass2.init(WorkerClass1.someInitializedValue);
        // etc.
    }
}

정리하자면 static final keyword가 선언된 변수를 초기화하는 과정이 복잡하다면 static block을 사용하기 적합합니다.


2. instance block

자바에서 instance block은 객체가 생성될때마다 실행됩니다. class는 여러개의 static block을 가질 수 있습니다.

neccessity of instance block
instance block은 익명 클래스에서 유용합니다. 왜 그럴까요? Java Language Specification, section 15.9.5.1에 따르면 다음과 같이 적혀있습니다.

An anonymous class cannot have an explicitly declared constructor.

익명 클래스에서는 explicit constructor를 가질 수 없다는 내용입니다. 즉 생성자에 접근할 일이 있다면 생성자에 접근하는 대신 instance block을 통해 해결가능합니다. 실제로 GZIPOutputStream에서는 compression level을 정의할 생성자나 api call이 없습니다. 이를 정의하기 위해 GZIPOutputStream을 상속하는 subclass를 만들어야하는데 이 경우 subclass를 만드는 대신 익명 클래스와 instance block을 활용하여 작성하면 편리합니다.

OutputStream os = . . .;
OutputStream gzos = new GZIPOutputStream(os) {
    {
        // def is an inherited, protected field that does the actual compression
        def = new Deflator(9, true); // maximum compression, no ZLIB header
    }
};

클래스 및 변수에서 사용될 수 있는 수식자

transient

transient를 이해하기 위해서는 Serialization을 이해해야합니다.

Serialization?
자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술

컴퓨터는 각기 다른 OS를 사용하는데 이때 OS마다 다른 가상 메모리 주소 공간을 갖기 때문에 Reference Type의 데이터는 전달할 수 없습니다. 이를 위해 주소 값이 아닌 바이트 형태로 직렬화된 데이터 값을 전달해야합니다. 직렬화된 데이터는 모두 원시 타입이 되어 파일 저장 혹은 네트워크 전송 시 파싱 가능한 유의미한 데이터 집합이 됩니다.

만약 변수에 transient 를 사용하면 해당 변수는 serialized 되지 않습니다. transient field는 객체의 serialized form에 존재하지 않기 때문에 deserialized process 진행 시 해당 필드는 기본값으로 초기화 됩니다.

volatile

해당 지정자가 붙은 변수에 대해서는 쓰레드가 값을 캐싱하지 않고 메인 메모리에서 처리하게 됩니다.

단일 Writer Thread와 여러 개의 Reader Thread가 존재하는 경우를 가정하겠습니다. CPU는 메모리에 직접 접근하여 프로세스를 처리하게 되면 성능적인 문제가 발생하므로 여러개의 캐시를 두어서 항상 메모리에 접근하는 것을 방지합니다.이에 따라 동기화 문제가 발생하는데 volatile keyword를 사용함으로써 해결가능합니다. Writer Thread가 여럿 존재한다면 AtomicVariable을 사용해야합니다.

synchorinzed

해당 지정자가 붙은 메서드와 스코프에 스레드간 동기화를 진행합니다. 자세한 내용은 다음에 다루도록 하겠습니다.

native

해당 지정자가 붙은 메서드는 Java 가 아닌 네이티브 코드를 사용합니다. 네이티브 코드란 C/C++ 등으로 작성된 DDL이나 JNI에서 제공하는 코드를 의미합니다.

strictfp

float나 double은 부동 소수점 연산시 정확하지 않은데 해당 키워드를 클래스나 메서드에 지정하는 경우 부동소수점 숫자의 정밀도를 보장할 수 있습니다.

final

해당 지정자가 사용된 것에 대해 상속이나 변경을 금지합니다. 예를 들어 변수에 사용되면 한번 초기화 된 이후로 수정할 수 없음을 의미하고 메서드에 사용되면 오버라이딩을 금지, 클래스에 사용되면 해당 클래스를 상속할 수 없음을 의미합니다.

로버트 마틴의 클린코드를 참고하면 기본적으로 코드 가독성을 해치지 않고 명시적으로 final 선언이 필요한 부분에 사용하라고 명시되어 있습니다.

abstract

클래스, 메서드에서 구현부를 가지지 않고 상속 시 구현을 강제하는 지정하는 키워드입니다. 추상 클래스 및 인터페이스에서 사용됩니다.

static

해당 클래스가 인스턴스화 되어있지 않아도 사용 가능한 키워드입니다. 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받아 여러 영역으로 나누어 관리합니다. 코드에서 사용되는 클래스들을 클래스 로더로 읽어 클래스마다 Runtime Constant pool, field data 등을 분류해서 저장합니다. 이 과정에서 static 변수들은 method area에 저장됩니다.


객체를 생성하는 방법

new keyword를 사용해서 객체를 생성할 수 있습니다. 사용 시, constructor를 실행하고 Heap에서 객체를 위한 저장공간을 할당받아 해당 공간을 참조할 메모리 주소를 돌려받게 됩니다.

Point originOne = new Point(23, 94);

Point Class는 다음과 같습니다.

public class Point {
    public int x = 0;
    public int y = 0;
    //constructor
    public Point(int a, int b) {
        x = a;
        y = b;
    }
}

생성자는 class의 이름과 동일하고 return type이 없는 것을 확인할 수 있습니다.


메소드를 정의하는 방법

메소드는 다음과 같은 방법으로 정의할 수 있습니다.

public double calculateAnswer(double wingSpan, int numberOfEngines,
                              double length, double grossTons) {
    //do the calculation here
}

일반적으로 아래와 같은 요소로 이루어져 있습니다.

  1. Modifier
  2. Return type
  3. Method Name
  4. parameters
  5. exception list
  6. method body

메서드 시그니처

메서드 명과 파라미터의 데이터타입들을 모아놓은 것을 의미합니다.

calculateAnswer(double, int, double, double)

Naming convention

identifier를 만족한다면 메서드 명으로 사용가능하지만 convention으로 사용하는 메서드 규칙이 있습니다. 소문자로 시작하는 동사이거나 여러 단어로 이루어진 경우에는 두번째 이상의 글자부터 대문자로 적어야하는 것입니다. 아래는 그 예시들입니다.

  • run
  • runFast
  • getBackground
  • compareTo
  • setX
  • isEmpty

생성자 정의하는 방법

클래스의 생성자는 객체를 생성할 때 호출됩니다. 메서드 명으로 클래스의 이름을 사용한다는 점과 return type이 없는 점을 제외하면 메서드를 정의하는 방법과 일치합니다. 아래는 Bicycle 클래스의 생성자 예시입니다.

public Bicycle(int startCadence, int startSpeed, int startGear) {
    gear = startGear;
    cadence = startCadence;
    speed = startSpeed;
}

Bicycle 객체를 생성하기 위해서는 new 를 사용할 수 있습니다.

Bicycle myBike = new Bicycle(30, 0, 8);

메서드와 마찬가지로 method signature가 다르다면 여러개의 생성자를 사용할 수 있습니다.


this keyword

인스턴스의 메서드나 생성자 내에서 this는 현재 객체에 대한 참조를 의미합니다.

사용하는 이유

Disambiguating Field Shadowing

this를 사용하는 가장 흔한 이유는 instance variable이 method나 constructor parameter에 의해 가려지기 때문입니다.

public class Point {
    public int x = 0;
    public int y = 0;

    //constructor
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

이 경우는 argument에 의해 public int x, public int y가 가려지기 때문에 this keyword를 통해 instance variable에 접근함을 알 수 있습니다.

Referencing Constructors

.this()를 통해서 동일한 클래스 내의 다른 constructor를 호출하는 경우에도 사용할 수 있습니다. 일반적으로는 parameterize constructor에서 default constructor를 호출하는 경우에 사용합니다.

public Person(String name, int age) {
    this();

    // the rest of the code
}

반대로 default constructor에서 parameterized constructor를 사용하여 전달할 수도 있습니다.

public Person() {
    this("John", 27);
}

.this()는 constructor에서 첫번째 statement에 위치하지 않으면 compilation error가 발생합니다.

Passing this as a parameter & returning this

this를 파라미터로 전달하거나 return 할 수 있습니다. 이를 활용해서 만약 클래스의 생성자가 복잡하다면 builder pattern으로 리팩토링할 수 있습니다. 아래는 builder pattern에서 this를 활용한 예시입니다.

image

출처