싱글톤 패턴의 기본 형태
싱글톤 패턴은 애플리케이션 전반에 걸쳐 단일 인스턴스만 존재하도록 보장하는 디자인 패턴이다. 가장 간단한 구현 방식부터 시작해보자.
즉시 초기화 (Eager Initialization)
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
클래스 로딩 시점에 인스턴스를 생성하므로 멀티스레드 환경에서도 안전하지만, 사용 여부와 상관없이 메모리를 차지한다는 단점이 있다.
게으른 초기화 + 이중 체크 잠금 (Double-Checked Locking)
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton() {}
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
volatile 키워드를 사용해 지시어 재배치 문제를 방지하고, 동기화 블록 내에서 다시 한 번 null 체크를 수행함으로써 성능과 스레드 안정성을 동시에 확보한다.
리플렉션에 의한 싱글톤 파괴 시도
자바 리플렉션 API를 이용하면 private 생성자에도 접근할 수 있어 싱글톤이 무력화될 수 있다.
public class ReflectionAttackDemo {
public static void main(String[] args) throws Exception {
LazyDoubleCheckSingleton obj1 = LazyDoubleCheckSingleton.getInstance();
Constructor<LazyDoubleCheckSingleton> c =
LazyDoubleCheckSingleton.class.getDeclaredConstructor();
c.setAccessible(true);
LazyDoubleCheckSingleton obj2 = c.newInstance();
System.out.println("obj1 == obj2 ? " + (obj1 == obj2)); // false
}
}
위 코드는 두 개의 서로 다른 인스턴스를 생성하여 싱글톤 원칙을 위반한다.
방어 시도: 상태 플래그 사용
public class DefensiveSingleton {
private static boolean created = false;
private DefensiveSingleton() {
synchronized (DefensiveSingleton.class) {
if (created) {
throw new IllegalStateException("Already initialized.");
}
created = true;
}
}
// ... getInstance 구현
}
하지만 이 방법조차 리플렉션으로 created 필드를 조작하면 우회 가능하다.
Field field = DefensiveSingleton.class.getDeclaredField("created");
field.setAccessible(true);
field.set(null, false); // 상태 리셋 → 새로운 인스턴스 생성 가능
완벽한 해결책: 열거형(ENUM) 사용
자바에서는 enum을 통해 리플렉션 공격까지 완벽히 차단할 수 있는 싱글톤을 구현할 수 있다.
public enum EnumBasedSingleton {
INSTANCE;
public void doSomething() {
System.out.println("Action executed by " + this.hashCode());
}
}
이 방식은 다음 이유로 안전하다:
- JVM은 enum 인스턴스 생성을 클래스 로딩 시 단 한 번만 허용한다.
- 리플렉션을 통한 인스턴스 생성 시도 시
IllegalArgumentException이 발생한다. - 직렬화/역직렬화 과정에서도 유일성이 유지된다.
실제 공격 시도 예시:
Constructor<EnumBasedSingleton> c =
EnumBasedSingleton.class.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
// java.lang.IllegalArgumentException: Cannot reflectively create enum objects
EnumBasedSingleton newInstance = c.newInstance("FAKE", 999);
자바 언어 사양상 enum 생성자는 리플렉션으로 호출할 수 없도록 강제되어 있다.
결론
전통적인 싱글톤 구현 방식들은 리플렉션이나 직렬화 공격에 취약할 수 있다. 반면 enum 기반 싱글톤은 언어 차원에서 유일성을 보장하므로, 최고 수준의 안정성이 요구되는 경우 가장 적합한 선택이다.