본문 바로가기

Java/Java SE, EE

[Java/Java SE, EE] 클래스(Class), 정적(static) 멤버와 정적 초기화 블록

클래스(Class)

자바의 클래스는 객체(Object)를 생성하기 위한 명세서입니다. 예를 들어, 자동차를 만들기 위한 설계도를 프로그래밍에서 클래스라고 부르고, 설계도로부터 생산된 자동차는 객체라고 부릅니다. 객체는 인스턴스(Instance)라고도 부릅니다. 클래스를 통해 객체-인스턴스를 만드는 과정을 인스턴스화라고 합니다. 어떤 공산품의 설계도를 그리는 이유는 동일한 제품을 설계도 그대로 똑같이 양산하기 위함입니다. 마찬가지로 프로그래밍에서도 클래스를 통해 여러 개의 객체를 생성 할 수 있습니다.

[접근 지정자] [클래스 이름] [코드 블록]

자바에서 클래스를 선언하기 위해서는 접근 지정자, 클래스 이름, 코드 블록을 사용합니다.

  • 접근 지정자
    public, protected, private, default 지정자를 사용하여 클래스의 접근 대상을 규정합니다.
  • 클래스 이름
    일반적으로 첫 문자는 알파벳 대문자로 시작하고, 이어지는 문자는 알파벳 소문자로 작성합니다. 자바 예약어(for, while, if 등)을 사용 할 수 없으며, 첫 번째 문자로는 숫자를 사용 할 수 없습니다.
  • 코드 블록
    클래스를 구성하는 필드, 메소드 등을 작성합니다.
public class Car { }

실행 목적, 라이브러리 목적

클래스는 크게 실행 또는 라이브러리 목적으로 구분됩니다. 우리가 프로그래밍을 할 때는 두 용도를 따로 고려하여 개발하지는 않습니다. 통상적으로 작성하는 모든 클래스는 라이브러리 목적이기 때문입니다.

실행을 위한 클래스는 자바 애플리케이션을 실행시키기 위한 용도입니다. 우리가 작성한 애플리케이션을 IDE 등에서 실행시킬 때 main() 함수를 포함하는 클래스가 실행 목적의 클래스입니다.

public static void main(String[] args) { }

인스턴스화

인스턴스화는 객체를 생성하는 행위입니다. 자바에서 객체를 생성하기 위해서 new 키워드를 사용합니다. new 키워드에 이어서 클래스의 생성자를 호출합니다. 클래스 생성자는 클래스를 인스턴스화 하는 일종의 함수에 해당합니다.

new [클래스 생성자]

클래스로 생성된 객체-인스턴스는 변수 등에 저장 할 수 있습니다. 객체를 저장하는 변수 타입은 클래스 이름입니다. 또한 객체는 힙 영역(Heap area)에 메모리를 할당 받기 때문에, 변수 타입은 참조(Reference) 타입에 해당합니다.

Car car = new Car();

구성

클래스는 크게 다음 세 가지 요소로 구성됩니다. 

  • 필드(Field)
    필드는 객체의 속성입니다. 객체가 갖는 변수로써, 같은 필드에서도 각 객체가 고유의 값을 갖습니다. 멤버 변수라고도 부릅니다.
  • 메소드(Method)
    필드는 객체의 행동입니다. 객체가 실행 할 수 있는 함수로써, 어떤 객체라도 클래스에 정의된 동일한 행동을 실행 할 수 있습니다. 멤버 함수라고도 부릅니다.
  • 생성자(Constructor)
    생성자는 객체를 인스턴스화 하는 메소드의 일종입니다. 모든 생성자의 이름은 클래스 이름과 동일합니다.

생성자는 new 키워드와 함께 사용하며, 생성자를 호출하면 인스턴스를 리턴 받습니다.

Car car = new Car();

클래스를 선언하였으나, 객체를 생성하지 않으면 해당 클래스가 정의하는 필드와 메소드에 접근 할 수 없습니다. 필드와 메소드에 접근하기 위해서 도트(.) 연산자를 사용합니다. 객체를 담고있는 변수 이름에 도트(.) 연산자를 붙이고, 이어서 필드 또는 메소드 이름으로 접근합니다.

int get_some_field = car.some_field;
int get_some_method_result = car.some_method();

객체 내부에서 객체가 갖고 있는 필드 또는 메소드를 호출 할 수도 있습니다. 객체 내부에서 자신의 데이터에 접근할 때는 this 키워드를 사용하며, 이어지는 필드 또는 메소드 이름 사이에 도트(.) 연산자를 사용합니다.

this.some_field = 100;
this.some_method();

생성자

생성자는 인스턴스화 단계에서 객체가 갖는 필드를 초기화하거나, 메소드를 호출함으로써 객체 생성 단계에서 필요한 로직을 처리합니다. 앞서 소개했듯이 생성자로 만들어진 인스턴스는 힙(Heap) 영역에 메모리를 할당 받습니다.

모든 클래스는 기본 생성자(Default constructor)를 갖습니다. 클래스를 선언 할 때 생성자를 생략하더라도, 컴파일러가 바이트 코드를 생성하면서 자동으로 기본 생성자를 추가합니다. 이렇게 자동으로 추가된 기본 생성자의 접근 지정자(public, protected, private, default)는 클래스의 접근 지정자를 따릅니다. 만약 클래스의 접근 지정자가 public이라면, 컴파일러가 추가한 기본 생성자는 다음 코드와 같습니다.

public Car() { }

생성자를 선언하기 위해서 접근 지정자, 클래스 이름, 파라미터, 코드 블록을 사용합니다. 모든 생성자의 이름은 클래스 이름과 동일합니다. 또한 메소드임에도 불구하고, 리턴 타입을 지정 할 수 없고 항상 해당 클래스의 타입을 리턴합니다.

[접근 지정자] [클래스 이름] [파라미터] [코드 블록]

코드에서 생성자를 명시적으로 작성하면, 컴파일러는 기본 생성자를 생성하지 않습니다. 만약 아래 코드처럼 어떤 파라미터를 전달받는 생성자를 명시적으로 작성하면, 더 이상 이 클래스를 new Car()처럼 기본 생성자를 사용하여 인스턴스화 할 수 없습니다.

public Car(int _some_parameter) { }

생성자 오버로딩

어떤 메소드는 같은 이름으로 여러 개 정의 될  수도 있습니다. 이를 오버로딩(Overloading)이라고 부르며, 클래스의 생성자 역시 오버로딩을 통해 여러 개의 생성자를 선언 할 수 있습니다. 오버로딩을 위해서는 이미 정의 된 동일한 이름의 메소드와 파라미터의 타입, 개수, 순서가 달라야 합니다.

public Car() { }
public Car(int _some_parameter) { }
public Car(int _some_parameter_0, int _some_parameter_1) { }
public Car(String _some_parameter) { }

앞서 객체 내부에서 자신의 데이터에 접근하기 위해 this 키워드를 사용한다고 소개했습니다. 클래스의 생성자 내부에서는 this() 코드를 사용하여 자신의 다른 생성자를 호출 할 수 있습니다. 호출하려는 생성자는 오버로딩 되어 있으며, 전달되는 파라미터에 따라서 오버로딩 된 생성자 중 하나가 호출됩니다. this()의 호출은 생성자 간에만 가능하며, 생성자의 코드 블록에서 가장 첫 줄에 입력합니다.

public Car(int _some_parameter_0) {
   this.some_field_0 = _some_parameter;
}

public Car(int _some_parameter_0, int _some_parameter_1) { 
   this(_some_parameter_0);
   
   this.some_field_1 = _some_parameter_1;
}

생성자에서 오버로딩 된 다른 생성자를 호출하는 이유는 중복되는 코드를 줄이기 위함입니다. this()가 호출되면 해당 생성자를 먼저 실행하고, 돌아와서 호출자 생성자의 나머지 코드를 실행합니다. 위 예제에서는 생성자를 오버라이딩하고 있지만, 클래스의 메소드-멤버 함수 역시 같은 방식으로 오버라이딩 할 수 있습니다.

정적 필드, 정적 멤버 변수

클래스 내부에 정적(static) 키워드를 사용하여 정적 필드-정적 멤버 변수를 선언 할 수 있습니다. 정적 멤버 변수는 클래스에 고정된 멤버로써 객체를 통하지 않고도(생성하지 않고도) 외부(또는 내부)에서 접근 할 수 있습니다.

public class Car {
   public static int SOME_STATIC_FIELD = 100;
}

자바 애플리케이션이 실행되면, 클래스 로더가 클래스(바이트 코드)를 클래스 로딩합니다. 클래스 로딩 단계에서 메소드 영역(Method area)에는 각 클래스 정보가 적재됩니다. 정적 필드는 객체를 생성하지 않고도 클래스 선언만으로 접근 할 수 있다고 했습니다. 이는 정적 필드가 메소드 영역에서 해당 클래스 정보와 함께 적재되기 때문입니다.

int get_some_static_field = Car.SOME_STATIC_FIELD;

정적 필드는 클래스 이름과 정적 필드 사이에 도트(.) 키워드를 사용하여 접근합니다. 객체를 담고있는 변수를 사용해서도 접근 할 수 있지만, 이는 일반 필드와 혼동되기 때문에 권장하지 않습니다.

정적 메소드, 정적 멤버 함수

정적 메소드 또한 클래스 내부에 정의 할 수 있습니다. 정적 필드처럼 클래스 외부 및 내부에서 도트(.) 연산자를 사용하여 접근 할 수 있으며, 이는 메소드 영역에 정적 메소드가 함께 적재되기 때문입니다.

public static int some_static_method() {
   return 100;
}

정적 초기화 블록

정적 필드는 다음과 같이 선언과 동시에 초기화를 진행하는 것이 일반적입니다.

int get_some_static_field = Car.SOME_STATIC_FIELD;

클래스의 필드는 인스턴스화가 진행되면서 초기화가 함께 진행됩니다. 만약 객체를 사용하기 전에 필드의 값을 바꾸고 싶다면, 생성자 또는 메소드 등에서 얼마든지 처리할 기회가 있습니다. 반면 정적 필드는 선언 이후에 값을 바꾸기 어렵습니다. 물론, 값을 바꾸는 것에 제약은 없습니다. 하지만 객체를 생성하지 않더라도 접근 할 수 있다는 특징으로 인해서, 아직 초기화를 하지 않은 정적 필드에 다른 코드가 접근하지 않았다는 보장은 어렵습니다.

모든 정적 필드가 선언과 동시에 초기화 할 수 있으면 문제 없습니다. 하지만 복잡한 연산식 등으로 초기화가 필요한 경우, 정적 초기화 블록을 사용 할 수 있습니다.

public static int SOME_STATIC_FIELD;    // 이 정적 필드는 아래의 정적 초기화 블록에서 다시 한번 초기화됩니다.

static {
   SOME_STATIC_FIELD = 100 * 4 * 13 / 99 - 200;
   System.out.println(SOME_STATIC_FIELD);
}

정적 초기화 블록은 static { }으로 구성되며, 메소드처럼 작성 할 수 있습니다. static { } 블록 내 코드는 애플리케이션이 실행 중일 때 클래스 로딩 단계에서 단 1회 호출됩니다. 따라서 이 정적 필드에 접근하는 어떤 경로에서도 정적 초기화 블록이 먼저 호출됩니다. 자바의 클래스 로딩은 런타임 중에 클래스에 대한 접근이 시작되었을 때 진행됩니다. 다시 말해서 예시의 SOME_STATIC_FIELD를 초기화하고 출력하는 코드는, 해당 클래스에 접근하는 코드가 없으면 실행되지 않습니다.