싱글턴 패턴(Singleton Pattern)은 특정 클래스의 인스턴스가 애플리케이션 전반에 걸쳐 오직 하나만 존재하도록 보장하고, 이에 대한 전역 접근 포인트를 제공하는 생성 디자인 패턴입니다. Java 언어를 기준으로 다양한 싱글턴 구현 기법과 각 방식의 스레드 안전성을 분석합니다.
1. 즉시 초기화 방식 (Eager Initialization)
클래스가 JVM에 로드되는 시점에 정적 변수를 통해 인스턴스를 미리 생성하는 방식입니다. 클래스 로딩과 초기화 과정이 JVM 수준에서 원자적으로 수행되므로 별도의 동기화 처리 없이도 스레드 안전성이 보장됩니다.
public class ImmediateSingleton {
private static final ImmediateSingleton SINGLE_INSTANCE = new ImmediateSingleton();
private ImmediateSingleton() {
// 외부에서의 인스턴스화 방지
}
public static ImmediateSingleton getSingleInstance() {
return SINGLE_INSTANCE;
}
}
2. 안전하지 않은 지연 초기화 방식 (Unsafe Lazy Initialization)
인스턴스가 실제로 필요해질 때까지 생성을 미루는 방식입니다. 구현이 직관적이지만, 멀티스레드 환경에서 여러 스레드가 동시에 조건문에 진입할 경우 여러 개의 인스턴스가 생성될 수 있어 스레드 안전하지 않습니다.
public class UnsafeLazySingleton {
private static UnsafeLazySingleton singletonObj;
private UnsafeLazySingleton() {}
public static UnsafeLazySingleton getSingletonObj() {
if (singletonObj == null) {
singletonObj = new UnsafeLazySingleton();
}
return singletonObj;
}
}
3. 동기화된 지연 초기화 방식 (Synchronized Lazy Initialization)
인스턴스를 반환하는 메서드 자체를 동기화하여 멀티스레드 환경에서의 스레드 안전성을 확보합니다. 하지만 인스턴스가 이미 생성된 이후에도 메서드 호출 시마다 불필요하게 락(Lock)을 획득해야 하므로 성능 병목 현상이 발생할 수 있습니다.
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton sharedInstance;
private SynchronizedLazySingleton() {}
public static synchronized SynchronizedLazySingleton getSharedInstance() {
if (sharedInstance == null) {
sharedInstance = new SynchronizedLazySingleton();
}
return sharedInstance;
}
}
4. 이중 확인 잠금 방식 (Double-Checked Locking)
동기화 블록의 범위를 최소화하여 성능을 개선한 방식입니다. 인스턴스 생성 여부를 먼저 확인하고, 생성되지 않았을 때만 동기화 블록에 진입하여 다시 확인합니다. 메모리 가시성 문제와 명령어 재배치를 방지하기 위해 변수에 volatile 키워드를 반드시 사용해야 합니다.
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton uniqueInstance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (DoubleCheckSingleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new DoubleCheckSingleton();
}
}
}
return uniqueInstance;
}
}
5. 정적 내부 클래스 방식 (Initialization-on-demand Holder Idiom)
JVM의 클래스 로딩 메커니즘을 활용하여 구현하는 방식입니다. 정적 내부 클래스는 외부 클래스가 로드될 때 함께 초기화되지 않으며, 내부 클래스의 정적 멤버가 처음 참조될 때 비로소 로드됩니다. 이를 통해 지연 로딩과 스레드 안전성을 동시에 달성할 수 있습니다.
public class HolderSingleton {
private HolderSingleton() {}
private static class LazyHolder {
private static final HolderSingleton HOLDER_INSTANCE = new HolderSingleton();
}
public static HolderSingleton getHolderInstance() {
return LazyHolder.HOLDER_INSTANCE;
}
}
6. 열거형 방식 (Enum Singleton)
Java의 열거형(Enum) 타입을 활용한 구현 방식입니다. 언어 스펙 수준에서 인스턴스의 유일성을 보장하며, 직렬화(Serialization) 과정에서의 인스턴스 중복 생성 문제와 리플렉션(Reflection) 공격으로부터 완전히 안전합니다. 가장 간결하고 권장되는 방식입니다.
public enum EnumBasedSingleton {
SINGLETON;
public void executeBusinessLogic() {
// 비즈니스 로직 수행
}
}
구현 방식별 특성 비교
| 구현 기법 | 스레드 안전성 | 지연 로딩(Lazy Loading) | 권장 여부 |
|---|---|---|---|
| 즉시 초기화 | 보장됨 | 미지원 | 리소스 부담이 없을 시 사용 |
| 안전하지 않은 지연 초기화 | 보장 안 됨 | 지원 | 단일 스레드 환경만 사용 |
| 동기화된 지연 초기화 | 보장됨 | 지원 | 성능 저하로 비권장 |
| 이중 확인 잠금 | 보장됨 | 지원 | 복잡도로 인해 신중히 사용 |
| 정적 내부 클래스 | 보장됨 | 지원 | 권장 |
| 열거형 | 보장됨 | 지원 (JVM 내부적) | 가장 권장 |