싱글톤 패턴(Singleton Pattern)은 애플리케이션 전체에서 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 이에 대한 전역 접근 포인트를 제공하는 디자인 패턴입니다. 인스턴스의 생성을 클래스 자체에서 제어하며, 메모리 낭비를 방지하고 일관된 상태를 유지하는 데 유용합니다. Java에서 싱글톤 패턴을 구현하는 다양한 방식과 그 특성을 분석해 봅니다.
1. Eager Initialization (즉시 초기화 방식)
클래스 로딩 시점에 정적 변수를 통해 인스턴스를 미리 생성하는 방식입니다. 구현이 간단하고 스레드 안전(Thread-safe)하지만, 인스턴스가 사용되지 않더라도 메모리에 상주하게 되어 자원 낭비가 발생할 수 있습니다.
public class EagerSingleton {
private static final EagerSingleton UNIQUE_INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return UNIQUE_INSTANCE;
}
}
2. Lazy Initialization with Synchronized Method (동기화 메서드 지연 초기화)
인스턴스가 필요할 때 생성하는 지연 초기화(Lazy Initialization) 방식입니다. getInstance() 메서드에 synchronized 키워드를 사용하여 스레드 안전성을 보장하지만, 메서드 호출 시마다 락(Lock)을 획득해야 하므로 성능 저하가 발생합니다.
public class LazySyncMethodSingleton {
private static LazySyncMethodSingleton uniqueInstance;
private LazySyncMethodSingleton() {}
public static synchronized LazySyncMethodSingleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new LazySyncMethodSingleton();
}
return uniqueInstance;
}
}
3. Synchronized Block (동기화 블록 사용 - 스레드 안전하지 않음)
성능 저하를 줄이기 위해 메서드 전체가 아닌 인스턴스 생성 부분에만 동기화 블록을 적용한 방식입니다. 하지만 두 개의 스레드가 동시에 if (uniqueInstance == null) 조건을 통과할 수 있어, 다중 인스턴스가 생성되는 심각한 스레드 안전성 문제가 발생합니다.
public class UnsafeSyncBlockSingleton {
private static UnsafeSyncBlockSingleton uniqueInstance;
private UnsafeSyncBlockSingleton() {}
public static UnsafeSyncBlockSingleton getInstance() {
if (uniqueInstance == null) {
synchronized (UnsafeSyncBlockSingleton.class) {
uniqueInstance = new UnsafeSyncBlockSingleton();
}
}
return uniqueInstance;
}
}
4. Broken Double-Checked Locking (이중 체크 잠금 - volatile 미적용)
동기화 블록 내부에 한 번 더 null 체크를 추가하여 다중 인스턴스 생성 문제를 해결하려는 방식입니다. 그러나 volatile 키워드가 누락되어 있어, JVM의 명령어 재배치(Instruction Reordering)로 인해 초기화되지 않은 불완전한 인스턴스가 반환될 수 있는 위험이 있습니다.
public class BrokenDclSingleton {
private static BrokenDclSingleton uniqueInstance;
private BrokenDclSingleton() {}
public static BrokenDclSingleton getInstance() {
if (uniqueInstance == null) {
synchronized (BrokenDclSingleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new BrokenDclSingleton();
}
}
}
return uniqueInstance;
}
}
5. Safe Double-Checked Locking (volatile을 적용한 이중 체크 잠금)
앞선 방식의 명령어 재배치 문제를 해결하기 위해 volatile 키워드를 적용한 방식입니다. 인스턴스 생성 시 메모리 할당과 초기화 순서를 보장하여 스레드 안전성을 확보하면서도, 인스턴스가 생성된 이후에는 동기화 블록을 거치지 않아 성능 저하를 최소화합니다.
public class SafeDclSingleton {
private static volatile SafeDclSingleton uniqueInstance;
private SafeDclSingleton() {}
public static SafeDclSingleton getInstance() {
if (uniqueInstance == null) {
synchronized (SafeDclSingleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new SafeDclSingleton();
}
}
}
return uniqueInstance;
}
}
6. Static Inner Class (정적 내부 클래스 활용)
Bill Pugh가 제안한 방식으로, JVM의 클래스 로딩 메커니즘을 활용하여 스레드 안전성과 지연 초기화를 모두 만족합니다. 내부 클래스는 외부 클래스가 로드될 때 함께 로드되지 않으며, getInstance()가 호출될 때 비로소 로드되어 인스턴스를 생성합니다.
public class InnerClassSingleton {
private InnerClassSingleton() {}
private static class InstanceHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return InstanceHolder.INSTANCE;
}
}
7. Enum (열거형 활용)
Java의 enum을 활용한 방식으로, Joshua Bloch가 권장하는 가장 간결하고 안전한 구현법입니다. 직렬화(Serialization) 과정에서의 인스턴스 다중 생성 문제와 리플렉션(Reflection)을 통한 생성자 호출 공격을 원천적으로 방지합니다.
public enum EnumSingleton {
UNIQUE_INSTANCE;
public static EnumSingleton getInstance() {
return UNIQUE_INSTANCE;
}
}