싱글톤 패턴 구현
싱글톤 패턴은 프로세스(Java에서는 JVM) 내에 1개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역 접근점을 제공하는 패턴이다.
즉시 초기화(Eager Initialization)
싱글톤 패턴을 가장 단순하게 구현하는 방법은 외부에서 해당 클래스의 인스턴스를 생성할 수 없도록 private 생성자를 만들고, 1개의 인스턴스만을 초기화(public static final)하여 이에 대한 접근을 제공하는 방식이다.
|
|
지연 초기화(Lazy Initialization)
Singleton의 static 필드나 메소드를 사용해야 하지만, 싱글톤 객체는 초기화시키지 않고 싶은 경우가 있을 수 있다. 이러한 경우에는 Lazy Initialization 방식을 사용할 수 있다.
Lazy Initialization 방식은 Singleton 객체가 필요할 때 이를 초기화(초기화되어 있지 않은 경우에만)하고, Singleton 객체를 반환받을 수 있는 진입점(static 메소드)를 제공하는 방식이다.
Singleton.java
|
|
실행 결과
|
|
명시적으로 getInstance
메소드를 호출하지 않는 이상 Singleton 객체(INSTANCE
)가 초기화되지 않기 때문에, Eager Initialization 방식의 단점을 보완할 수 있다.
Multi Thread 환경에서의 문제점
Lazy Initialization 방식은 Multi Thread 환경에서의 문제점을 가지고 있다. Multi Thread 환경에서 서로 다른 Thread들이 동시에 getInstance
를 호출한다고 생각해보자. 위의 getInstance
는 Multi Thread 환경에 대한 고려가 되어 있지 않기 떄문에, if(INSTANCE == null)
이 여러번 참인 경우가 발생하여 여러 개의 객체가 생성될 수 있다.
Singleton.java
|
|
실행 결과
|
|
Multi Thread 환경에서 Lazy Initialization 방법
- 정적 팩터리 메서드에 동기화 키워드 사용하기
- Singleton 객체를 생성하는 정적 팩터리 메서드 자체에
synchronized
를 붙여주거나, 내부 초기화 구문을synchronized
구문으로 감싸주는 방법이 있다.
- Singleton 객체를 생성하는 정적 팩터리 메서드 자체에
- 정적 팩터리 메서드 자체에 synchronized 붙이기
- 기존의
public static Singleton getInstance()
를public static synchronized Singleton getInstance()
와 같이 바꾸는 방법이다.
- 기존의
- 내부 초기화 구문을 synchronized로 감싸주기
- 초기화에 사용할 Lock 객체를 만들고, Singleton 객체의 초기화 여부 확인 시 동기화하는 방법이다.
|
|
동기화 방식은 Multi Thread 환경에서 객체를 안전하게 1개를 생성할 수 있지만, 객체 획득 시의 병렬성을 잃어버린다는 단점이 존재한다. 또한 초기 호출 이후에는 객체 초기화 확인 구문(if(INSTANCE) == null)
)이 계속해서 참을 반환하기 때문에 불필요한 Lock을 거는 상황이 발생하게 된다.
DCL(Double-Checked-Locking) 방식 사용하기
Multi Thread 환경에서 병렬성을 확보하기 위해서는 동기화 구문을 필요한 곳에서만 사용해야 한다. DCL 방식은 동기화를 최소화하므로써 불필요한 Lock을 거는 단점을 없앤 방식이다.
|
|
첫번째 if문을 통해 객체가 초기화되었는지 검사할 때는 동기화를 하지 않고, 두번째 if문을 통해 객체가 초기화되었는지 검사할 때는 동기화를 수행한다. getInstance
호출 후 충분한 시간이 지나 동기화가 필요 없을 때는 첫번째 if문만 실행하므로써 동기화에 따른 병렬성 감소를 감소시킬 수 있는 방식이다.
언뜻 보기에는 동기화 문제도 없고 성능적으로 뛰어난 방식이라고 생각할 수 있다. 그러나 위 코드는 문제점을 가지고 있다.
위 DCL 코드의 문제점과 해결
위 DCL 코드는 JVM의 Low-Level 동작 방식때문에 문제가 발생한다.
예를 들어 2개의 Thread(A, B)가 getInstance
를 호출했다고 생각해보자. Thread A는 INSTANCE = new Singleton()
구문을 수행하고 있고, 이 때 Thread B는 첫번째 if문에서 INSTANCE의 초기화 여부를 검사(if(INSTANCE == null)
)하고 있다. 여기서 Thread A가 수행하는 ‘객체 생성’과 ‘변수 할당’ 구문은 한번에 실행되지 않고, 특정 시점에는 INSTANCE에는 일부만 초기화된 객체가 할당되어 있을 수 있다. 이 때 Thread B는 non-null check만 하기 때문에 일부만 초기화된 객체를 가져다 쓰게 되어 Crash가 발생할 수 있다.
이러한 문제점을 해결하기 위해서는 INSTANCE 객체에 volatile
키워드를 붙여주면 된다. volatile
은 원자적으로 동작하기 때문에 첫번째 if문이 완전히 초기화된 객체를 얻거나 아닌 경우 둘 중 하나로 동작하게 된다.
Lazy Holder 방식 사용하기
Multi Thread 환경에서도 잘 동작하고, Lazy Initialization도 가능한 방식이다.
|
|
위 코드를 보면 ‘Lazy Initialization 방식이 아니지 않은가?’ 라고 생각할 수 있다. LazySingletonHolder
의 getInstance
에서 Singleton
클래스의 INSTANCE
를 그대로 반환하기 때문이다. 그러나 Singleton
클래스의 INSTANCE
는 Singleton
클래스의 클래스 로딩이 일어날 때 발생하게 되고, 클래스 로딩은 LazySingletonHolder
의 getInstance
가 호출되었을 때 발생한다. 또한 INSTANCE
는 static final
이기 때문에 클래스 로딩에 의해 1회만 초기화되고, 이로써 Multi Thread 환경에서의 문제점도 없어지게 된다.