본문 바로가기
프로그래밍/디자인 패턴

[java/Design Pattern] 싱글톤의 Lazy 초기화

by 사바라다 2020. 11. 28.

안녕하세요. 오랜 만에 여러분들과 Design 패턴에 대해서 알아보는 시간을 가져보려고 합니다. 이전에 저희들은 Singleton 패턴에 대해서 [Spring & Design Pattern] Spring에서 발견한 디자인패턴_Singleton Pattern 시간에 알아본적이 있습니다. 그때 Singleton을 초기화 할때 아래와 같은 코드로 초기화를 진행했습니다. 하지만 사실 이렇게 초기화 하는 것은 문제점을 안고 있습니다.

public class Government {

    private static Government government;

    private Government() {}

    public static Government election() {
        if(government == null)
        {
            government = new Government();
            return government;
        }
        return government;
    }
}

어떤 문제점일까요 ? 오늘은 이 문제점을 확인하고 이를 해결하는 방법에 대해서 알아보는 시간을 가지도록 하겠습니다.

위 예제의 문제점, Singleton의 Lazy 초기화

Singleton Pattern이란 인스턴스를 1개로 제한하며 global하게 어디서든 동일한 객체에 접근하여 사용할 수 있도록하는 객체생성 디자인 패턴입니다. 위의 예제로 다시 돌아가 보겠습니다. 위의 예제는 객체의 초기화 시점이 처음 Government.election()으로 접근했을 때 입니다. 즉 Lazy 로딩이라는 것을 알 수 있습니다. 언뜻 보기에는 문제가 없어보입니다. Government.election()으로 접근했을 때 instance에 할당된 객체가 없다면 새로운 인스턴스를 생성 후 할당 그리고 반환하며, 객체가 이미 존재한다면 존재하는 객체를 바로 반환하도록 로직이 구성되어 있습니다.

하지만 multi Thread 환경이라고 생각해보도록 하겠습니다. 2명의 사용자가 동시에 Governmnet.elevction() 이라는 메서드를 호출했습니다. 그렇다면 정확하게 Government 인스턴스는 한번만 만들어지게 되는 걸까요 ? 아닐껍니다. if(government == null)의 조건문에 대해서 여러 thread가 동시에 들어올 가능성이 존재하기 때문에 위험합니다. 이런 경우를 대비하여 thread safe 하게 singleton을 만들어야할 필요가 생겼습니다.

아래는 Singleton 객체 초기화를 위한 다양한 접근 방법들입니다. 오늘은 Lazy 초기화에 대해서만 알아보도록하고 Eager 초기화에 대해서는 다음시간에 이어서 알아볻록 하겠습니다.

  • Eager initialization: 클래스 인스턴스 생성을 실제로 사용되기전에 먼저 진행
    • Static block initialization
    • Enum singleton
  • Lazy initialization: 클래스 인스턴스의 생성을 처음 실제로 사용될 때 진행
    • Thread safe singleton
    • Bill Pugh singleton(static holder)

thread safe singleton

thread safe하게 만들수 있는 가장 쉬운 방법은 synchronized 수식어를 사용하는 것입니다. synchronized를 사용하면 해당 메서드는 여러 thread가 접근하게 됬을 시 한 thread만 접근하게 합니다. 나머지 thread는 해당 thread의 작업이 끝나기를 기다리게 됩니다. 그렇게 되면 instance는 이미 만들어졌기 때문에 나머지 thread는 이미 만들어져있는 인스턴스를 반화하게 됩니다.

public class Government {

    private static Government government;

    private Government() {}

    public static synchronized Government election() {
        if(government == null)
        {
            government = new Government();
            return government;
        }
        return government;
    }
}

하지만 위 처럼 메서드를 만들면 Government의 모든 instance에 접근하기 위해서는 synchronized 메서드를 거쳐야합니다. 하지만 synchronized는 성능상 부담스러운 부분이 있습니다. 왜냐하면 synchronized는 하나의 thread만 접근할 수 있기때문에 Government 인스턴스를 결국 1개의 thread만이 사용할 수 있다는 의미가 되는 것입니다. 이런 오버헤드를 피하기 위해서 double checked locking이 나왔습니다. double checked locking은 아래 처럼 인스턴스를 생성하는 경우에만 synchronized를 통해서 동기화를 취하게 하는것입니다.

public class Government {

    private static Government government;

    private Government() {}

    public static Government election() {
        if(government == null) {
            synchronized (Government.class) {
                if (government == null) {
                    government = new Government();
                    return government;
                }
            }
        }
        return government;
    }
}

Java 5 이전에서는 Java Memory Model의 불안정성에 의해서 double checked locking이 정상적으로 동작하지 않았던 이슈가 있었다고 합니다. (참고용)

Bill Pugh(Static Holder) singleton

synchronized는 결국 동시성에 제약을 걸어 thread safe를 만족시키는 것입니다. 하지만 synchronized 명령어를 이용하지 않고 Singleton을 thread safe하게 초기화하는 방법이 있습니다. 그게 바로 Bill Pugh Singleton이며 다르게는 static holder singleton 패턴이라고도 부릅니다.

사용하는 방법은 아래와 같습니다. 내부에 private static class를 만들고 사용하고 싶을 때 public static 메서드를 통해서 호출하는 것입니다.

public class Government {

    private Government() {}

    public static Government election() {
        return SingletonHelper.INSTANCE;
    }

    private static class SingletonHelper {
        private static final Government INSTANCE = new Government();
    }
}

이것은 private static class의 특성을 이용한 것입니다. private static class는 static class 이지만 메모리에 바로 올라가지 않습니다. 누군가가 election()을 호출 헸을 때만 메모리에 올라가게 되는 것입니다. 이렇게 해도 thread safe하지 않는게 아닐까요 ?

그렇지 않습니다. 이 방법은 Initialization-on-demand holder 개념을 이용한 것입니다. private static class는 JVM의 static initializer에 의해서 초기화 되고 메모리로 올라가게 됩니다. 최초로 ClassLoader에 의해 load 될 때 loadClass 메서드를 통해 올라가게 됩니다. 이 때 내부로 synchronized가 실행됩니다. 따라서 명시적으로 synchronized를 이용하지 않고도 동일한 효과를 낼 수 있습니다.

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
// ... 하략

마무리

오늘은 이렇게 멀티 스레드 환경에서 Lazy Initialization 상황에서 singleton을 초기화하는 방법에 대해서 알아보았습니다.

다음시간에는 Early Initialization 상황에서의 singleton을 초기화 하는 방법에 대해서 알아보도록 하겠습니다.

감사합니다.

참조

wikipedia_Initialization-on-demand_holder_idiom

stackoverflow_regarding-static-holder-singleton-pattern

dzone_initializing-singleton-idioms

journaldev_java-singleton-design-pattern-best-practices-examples

댓글