반응형

서론


이번에 GOF 디자인 패턴을 공부하면서 기선님 강의를 봤는데 챕터1인 싱글톤 패턴 부터 막혔다.

static을 몰랐기 떄문이다. 그래서 이번 기회에 조금 더 정리할 수 있는 기회가 되었다.

예전에도 헷갈렸던 것 같다. 이번 기회에 조금 더 알아가는 기회가 될 것 같다.

내가 이해한 static은 공유 자원 + 클래스 로드 시 같이 메모리에 올라가는 것이었다.

예전에도 공유라는 것에 현혹 되었던 적이 있는데 다시 초기화가 된 것 같다.

조금 더 알아 보자.

싱글톤 구현하기


강의에서 싱글톤 구현하는 방법을 몇 가지 배웠다. 기존에 알고 있는 방식은 잠재된 버그가

있다는 것을 알게 되었다. 코드를 살펴 보자.

멀티 쓰레드 환경에서 안전하지 않는 코드

public class Settings {

    private static Settings instance;

    private Settings() { }

    public static Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }
        return new Settings();
    }
}

이 코드는 쓰레드1이 null 체크를 끝내고 바로 쓰레드2도 null 체크를 하게 되면, 쓰레드1과 쓰레드2는

다른 인스턴스를 갖게 되기 때문에 Not Thread-Safe하다.

Synchronized 키워드 사용하여 멀티 쓰레드 환경에서 안전한 코드

public class Settings {

    private static Settings instance;

    private Settings() { }

    public static synchronized Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }
        return new Settings();
    }
}

위의 문제를 해결할 수 있는 방법이다.

쓰레드가 동시에 해당 메서드에 접근하지 못하도록 synchronized 키워드로 lock을 거는 방법으로 해결한다.

하지만 또 문제가 있다.

  • 매번 getInstance를 호출 시 lock이 걸리게 되어 성능 이슈가 발생한다.

이른 초기화(eager initiallization)

public class Settings {

    private static final Settings INSTANCE = new Settings();

    private Settings() { }

    public static Settings getInstance() {
        return INSTANCE;
    }
}

위의 성능 문제를 해결할 수 있는 방법이다.

간단하게 인스턴스만 미리 초기화하는 방식이다.

클래스가 로드되는 시점에 static 필드가 초기화가 되기 때문이다. static 블럭을 사용해서 해도 마찬가지 이다.

단점

  • 인스턴스 생성 비용이 클 경우 사용하는 당시 생성하는 효율적이다.
  • 즉, 힘들게 생성해 놨는데 메모리를 많이 잡아먹고, 사용하지 않는 상태가 발생하는 것이다.
  • 하지만 이런 일은 거의 고려할 필요가 없다고 생각한다.

double checked locking

public class Settings {

    private static volatile Settings instance;

    private Settings() { }

    public static Settings getInstance() {
        if (instance == null) {
            synchronized (Settings.class) {
                if (instance == null) {
                    instance = new Settings();
                }
            }
        }
        return instance;
    }
}

위의 문제를 해결할 수 있는 방법이다.

첫 번째 if를 체크하고 null이 아니면 기존에 있는 instance를 반환한다. null일 경우가 중요하다.

  1. 쓰레드1이 첫 번째 if에서 null을 확인하고, if 블록으로 진입
  2. 쓰레드2도 첫 번째 if에서 null을 확인하고, if 블록으로 진입
  3. 쓰레드1이 synchronized 키워드로 lock 획득
  4. 쓰레드2는 쓰레드1이 lock을 획득 했기 때문에 대기
  5. 쓰레드1은 if에서 null을 확인하고 new Settings() 실행하고 lock 반납
  6. 쓰레드2는 lock을 획득하고 if에서 null이 아닌 것을 확인하고, 기존에 있는 instance 반환
  7. 다음에 또 다시 요청을 하면 synchronized로 접근 자체를 하지 않기 때문에 문제를 해결할 수 있다.

이와 같은 흐름이다. 즉, 두 번 체크를 했기 때문에 double checked locking이라고 한다.

정리를 하자면,

장점

  • 필요한 시점에 인스턴스를 생성할 수 있다.(lazy)
  • 항상 lock이 걸리는 것이 아니기 때문에 속도 문제를 내지 않을 수 있다.

하지만 또 문제점

  • volatile을 사용하는 이유를 이해하기 힘들다. 자바 1.4이하 버전에서 멀티 쓰레드 환경에서
    메모리를 다루는 방법을 알아야 하는데 너무 복잡하다.
  • 1.5 이상에서만 동작한다. 혹시나 1.4를 쓸 경우가 있을 수 있다.

static inner class 사용하기

public class Settings {

    private Settings() {}

    private static class SettingsHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance() {
        return SettingsHolder.INSTANCE;
    }
}

이 방법은 권장하는 방법 중 하나로 위의 방법들에서 장점만 취하는 방법이다.

장점은 아래와 같다.

  • lazy 로딩으로 필요한 시점에 생성 가능
  • lock으로 인한 성능 감소 방지
  • 멀티 쓰레드 환경에서 안전하다.

static inner class 방법이 왜 lazy 초기화일까?


이 글을 쓰게 된 계기가 드디어 나온다.

내가 이해한 static

static 키워드가 있으면 무조건 클래스 로드 시 같이 초기화 되는 거 아닌가? 아니었다.

로드초기화라는 키워드를 분리해서 생각했어야 했다.

즉, static inner 클래스가 로드되는 시점은

  1. Settings.class
  2. 실제 사용할 때 이 과정에서 로딩 → 링크 → 초기화가 이루어 진다.

초기화가 되는 시점은 실제로 객체를 사용할 때 이다.

로드되는 시점 확인 해보기

public class Settings {

    // outer class의 static
    private static final String SETTINGS_INSTANCE = "Settings Instance";

    private Settings() {}

    static {
        System.out.println("SETTINGS_INSTANCE = " + SETTINGS_INSTANCE);
    }

    // static inner class
    private static class SettingsHolder {

        // Settings static final로 생성
        private static final Settings SETTINGS_IN_HOLDER = new Settings();

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

    public static Settings getInstance() {
        return SettingsHolder.SETTINGS_IN_HOLDER;
    }

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

/* 실행 결과 */
SETTINGS_INSTANCE = Settings Instance
main 실행

static inner class가 언제 로드되고, 초기화 되는지 확인 해보자.

  1. main 메서드 실행 시 Settings는 로드 되니까 SETTINGS_INSTANCE는 초기화 된다.
    • static 초기화 블록이 실행된 것.
  2. 그리고 main의 로그가 찍힌다.

당연한 결과이다. 먼저 클래스 로드를 하고 실행을 해야 하니까

static inner class는 메서드(메서드가 아닐 수도 있음)를 호출할 때 초기화가 된다

꼭 메서드가 아니어도 해당 클래스를 인스턴스화 하거나 필드를 사용하거나 하면 초기화가 된다.

일단, 위의 테스트로 클래스 로드가 되면, static 초기화 블록이 작동하는 것을 확인했다.

그러면 결국 static inner class는 로드가 되지 않은 것일까? 이게 문제였다.

실제로 로드 조차 되지 않은 것이 맞다. 로드가 되지 않았으니 초기화도 되지 않는다.

public class Settings {

    private static final String SETTINGS_INSTANCE = "Settings Instance";

    private Settings() {
        System.out.println("4 - Constructor Settings");
    }

    static {
        System.out.println("1 - Outer Class static initializer SETTINGS_INSTANCE = " + SETTINGS_INSTANCE);
    }

    public static void main(String[] args) {
        System.out.println("2 - main SETTINGS_INSTANCE = " + SETTINGS_INSTANCE);
        System.out.println("3 - SettingsHolder.class = " + SettingsHolder.class);
        System.out.println("6 - SettingsHolder.SETTINGS_IN_HOLDER " + SettingsHolder.SETTINGS_IN_HOLDER);
    }

    private static class SettingsHolder {

        private static final Settings SETTINGS_IN_HOLDER = new Settings();

        static {
            System.out.println("5 - SettingsHolder.static initializer");
        }

        private SettingsHolder() {
            System.out.println("Constructor SettingsHolder");
        }
    }
}

/* 실행 결과 */
1 - Outer Class static initializer SETTINGS_INSTANCE = Settings Instance
2 - main SETTINGS_INSTANCE = Settings Instance
3 - SettingsHolder.class = class Settings$SettingsHolder
4 - Constructor Settings
5 - SettingsHolder.static initializer
6 - SettingsHolder.SETTINGS_IN_HOLDER Settings@6e0be858

실행 결과 로그와 아래 번호는 무관하고, 상세하게 동작 방식을 알아보자.

  1. 먼저 main()을 실행한다.
  2. Outer인 Settings의 static 필드인 SETTINGS_INSTANCE는 클래스 로드 후 바로 초기화 된다.

→ 1 - Outer Class static initializer SETTINGS_INSTANCE = Settings Instance
3. Outer static 블록 초기화 후 2번 로그가 실행된다.
→ 2 - main SETTINGS_INSTANCE = Settings Instance
4. main()에서 SettingsHolder의 로드 여부를 확인한다. 클래스 로드는 된 것을 확인할 수 있다.
→ 3 - SettingsHolder.class = class Settings$SettingsHolder
5. main()에서 SettingsHolder의 어떤 정보인 static 필드인 SETTINGS_IN_HOLDER를 호출한다.
어떤 정보라는 것은 정말 아무거나 SettingsHolder의 무언가를 의미한다.
6. 해당 필드는 new Settings()가 필요하므로 Settings()의 생성자를 호출한다.
→ 4 - Constructor Settings(여기서는 이미 3번에서 로드가 됐으니 초기화만 이루어 진다.)
7. 이후 SettingsHolder의 static 필드는 초기화 되고. static 블록이 실행된다.
→ 5 - SettingsHolder.static initializer
8. 마지막으로 main()에서 6번 로그를 실행한다.
→ 6 - SettingsHolder.SETTINGS_IN_HOLDER Settings@6e0be858

조금 더 상세하게 로드와 초기화 시점을 알아 보자.

public class Settings {

    private static final Settings INSTANCE = new Settings();

    private Settings() { 
        System.out.println("Settings.Settings");
    }

    public static Settings getInstance() {
        return INSTANCE;
    }
}

public class Main {

    public static void main(String[] args) {
        System.out.println(Settings.class);
//        new Settings("main에서 생성");
    }
}

/* 실행 결과 */
class Settings

먼저 해당 코드는 로드만 일어나고 초기화는 일어나지 않는다.

즉, eager 초기화라고 할 수 있다. 하지만 나는 lazy 아닌가? 라고 생각했다.

클래스를 로드 했는데도 초기화가 되지 않았기 때문이다.

eager라는 의미

실제로 위 코드와 같이 사용하는 경우는 없다. 다른 메서드를 제공하기 마련인데, getInstance()를

호출하지 않고, 다른 메서드를 호출하게 되면, 로드도 되고, 바로 초기화도 되는 것이다.

즉, getInstance()를 사용하지도 않았는데 static이 초기화가 된 것이다. 그래서 eager 초기화라고 하는 것이다.

그래서 static inner class를 사용하면, lazy 초기화를 할 수 있는 것이다. 이유는 Outer 클래스인

Settings가 getInstance()외에 다른 메서드를 사용해도, static inner class인 SettingsHolder는

로드조차 되지 않기 때문이다.

로드 시점

로드 시점은 두 가지로 볼 수 있다.

  1. Settings.class 할 때
  2. 클래스의 기능을 사용하라 때로 메서드 호출 및 인스턴스 생성 등이다.초기화를 하기 위해 로드 → 링크 → 초기화 과정을 한 번에 거치게 된다.
  3. 이 때는 클래스의 무언가에 접근하는 것이기 때문에 초기화가 필요하고,

초기화 시점

실제 클래스의 메서드나 필드, 생성자 호출 등을 할 때 초기화가 이루어 진다.

SettingsHolder 예제로 정리를 하자면, Settings의 inner class인 SettingsHolder와 Settings는

.class나 클래스를 사용하지 않으면 로드도, 초기화도 아무 일도 일어나지 않는다.

이 말은 애플리케이션을 실행해서 가장 처음에 실행되는 main()을 가진 클래스가 로드 되고, 초기화 되어도,

모든 class가 로드되는 것이 아니라는 것이다. 일반적인 상황으로 본다면, .class 키워드를 사용하는 일 보다.

인스턴스화를 하거나, static 메서드만 있는 Util 클래스의 메서드를 호출할 때 초기화가 필요하기 떄문에

로드 → 링크 → 초기화가 한 번에 이루어 지게 된다. 추가로 오해한 부분이 있었는데 애플리케이션이 실행되면,

모든 클래스들이 로드는 되는 줄 알았다. 하지만 자바는 동적 로딩을 하는데 이는 위에 설명과 같이 .class나

클래스를 사용할 때 ClassLoader 클래스의 loadClass()를 호출하면서 이루어 지게 된다.

참고 자료


반응형

'Java' 카테고리의 다른 글

Java8 Optional 올바르게 사용하는 방법  (1) 2022.10.03
JDBC란 무엇인가?  (0) 2022.08.17
CountDownLatch로 동시성 테스트 하기  (0) 2022.05.14
Java Comparator, Comparable  (0) 2022.02.22
리플렉션으로 DI 컨테이너 만들기  (0) 2021.12.21
복사했습니다!