이번 글에서는 Java에서 클래스 정보를 불러오는 방법을 알아보고 Initailizer block 문법도 알아보도록 하겠습니다.

이 글은 Java 17을 기준으로 작성하였습니다. 참고 자료는 다음과 같습니다.

클래스 로더란?

클래스 로더(class loader)는 JRE의 일부로 Java 클래스를 메모리에 동적으로 불러오는 역할을 합니다. JVM이 클래스를 발견했을 때 클래스 정보가 없다면 클래스 로더에 클래스 정보를 요청하는데요. 이때 클래스 로더가 정규화된 클래스 이름으로 클래스 정보를 불러옵니다.

덕분에 JVM은 Java 애플리케이션에서 필요한 클래스들의 파일 정보를 몰라도 됩니다. 또 런타임에 동적으로 클래스 정보를 불러오기 때문에 선언한 클래스가 많더라도 사용하는 클래스만 메모리에 불러옵니다. 따라서 메모리를 절약한다는 장점도 있습니다.

클래스 로더도 클래스로 정의되어있는데…?

그런데 이러한 역할을 하는 클래스 로더도 ClassLoader라는 클래스에 정의되어있는데요.

Untitled.png

따라서 ClassLoader의 인스턴스가 Java 클래스들을 로드하게 됩니다. 이때 다음과 같은 의문이 들 수 있습니다.

클래스를 불러오는 것이 클래스 로더인데, 클래스 로더도 클래스면 ClassLoader는 누가 로드하지?

이러한 작업은 부트스트랩 클래스로더에 의해 이루어집니다. 네이티브 코드로 작성된 부트스트랩 클래스 로더가 ClassLoader 클래스를 로드합니다.

클래스 로더의 계층 구조

한편 클래스 로더는 계층 구조를 가지고 있는데요. 이를 도식화하면 다음과 같습니다.

Untitled.png

부트스트랩 클래스로더(bootstrap class loader)는 JVM이 시작할 때 먼저 실행되며 java.lang과 같이 JDK의 핵심 라이브러리 클래스를 불러옵니다. 참고로 ClassLoader은 java.lang에 존재합니다.

확장 클래스로더(extension class loader)는 부트스트랩 클래스로더 다음으로 실행되며 javax.servlet과 같이 JDK의 확장 라이브러리 클래스를 불러옵니다.

애플리케이션 클래스로더(application class loader)는 사용자가 작성한 애플리케이션 코드를 로드합니다.

클래스를 불러오는 과정

클래스 로더가 클래스를 불러오는 과정은 이러한 계층 구조를 기반으로 진행됩니다. JVM이 클래스를 요청하면 ClassLoader 인스턴스가 바로 처리하는 것이 아니라 부모 클래스 로더에게 요청을 위임합니다.

Untitled.png

Untitled.png

애플리케이션 클래스로더가 확장 클래스 로더에 요청을 위임하고 확장 클래스 로더는 다시 부트스트랩 클래스 로더에 요청을 위임합니다. 이때 부모 클래스에서 클래스를 불러오지 못할 경우 다시 자식 클래스에게 요청을 위임합니다. 만약 끝까지 찾지 못한 경우에는 ClassNotFoundException와 같은 예외를 던집니다. 이러한 위임 모델 덕분에 JVM은 고유한 클래스 정보를 쉽게 얻을 수 있습니다.

Initializer block

Initializer block은 Java 1.0부터 지원하는 문법으로 두 종류가 존재합니다.

  • 정적 초기화 블록(static initializer block)
  • 인스턴스 초기화 블록(instance initializer block)

정적 초기화 블록은 클래스가 로드되고 초기화 될 때 실행되는 명령어를 정의하는 곳입니다. 클래스가 로드될 때 실행되므로 main() 메서드보다 먼저 실행된다는 특징을 가지고 있습니다.

public class InitializerBlock {

    static {
        System.out.println("static initializer block");
    }

    public static void main(String[] args) {
        System.out.println("main method");
    }
}

Untitled.png

직접 클래스를 로드하거나 main() 메서드로 정적 초기화 블록을 실행할 수도 있지만 객체를 최초 생성할 때도 정적 초기화 블록이 실행됩니다.

@Test
void executeStaticInitializerBlockOnce() throws Exception {
    /* given */

    /* when */
    new InitializerBlock();
    new InitializerBlock();
    new InitializerBlock();

    /* then */
}

Untitled.png

인스턴스 초기화 블록은 클래스의 인스턴스를 생성하기 전에 실행되는 명령어를 정의하는 곳입니다. 생성자보다 먼저 실행된다는 특징을 가지고 있습니다.

@Test
void priorityBetweenConstructorAndInitializerBlock() throws Exception {
    /* given */

    /* when */
    new InitializerBlock("first instance");
    new InitializerBlock("second instance");
    new InitializerBlock("third instance");

    /* then */
}

Untitled.png

언제 사용할까?

사실 초기화 블록 문법은 직접 사용할 일이 거의 없고 함부로 사용해선 안됩니다. 정적 초기화 블록에서 생성한 객체는 method area에 저장되므로 클래스가 로드될 때부터 애플리케이션이 종료될 때까지 살아있어 메모리 누수가 발생할 수 있습니다.

또 인스턴스 초기화 블록의 경우 생성자와 거의 동일한 역할을 하므로 반드시 필요한 경우가 아니라면 생성자에서 처리하는 것이 좋습니다. 불필요하게 둘을 구분할 경우 가독성이 떨어지고 관리 포인트가 늘어나기 때문입니다.

마무리하며

이번 글에서는 JVM이 필요한 클래스 정보를 불러오는 클래스 로더에 대해 알아보았습니다. 클래스를 불러오는 과정을 이해한 후 정적 초기화 블록에 대해서 알아보았습니다.

사실 저는 초기화 블록이라는 것이 존재하는 지도 몰랐는데요. Testcontainers 라이브러리를 이용하여 Docker 기반 테스트 환경을 구축할 때 정적 초기화 블록이 등장하여 이 글을 작성하게 됐네요. 🤣

앞으로 초기화 블록을 사용할 일이 있을 지 모르겠지만 Java API나 Spring 코드를 볼 때 가끔 등장하면 당황하지 않을 것 같습니다. 또 덕분에 Java에서 클래스 정보를 불러오는 과정에 대해 자세히 알게 되었구요.

이번 글에서 사용한 코드는 이 곳에서 확인하실 수 있습니다.

카테고리:

업데이트:

댓글남기기