자바 싱글톤 패턴의 안전성과 리플렉션 공격 대응 방안

싱글톤 패턴의 기본 형태

싱글톤 패턴은 애플리케이션 전반에 걸쳐 단일 인스턴스만 존재하도록 보장하는 디자인 패턴이다. 가장 간단한 구현 방식부터 시작해보자.

즉시 초기화 (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 기반 싱글톤은 언어 차원에서 유일성을 보장하므로, 최고 수준의 안정성이 요구되는 경우 가장 적합한 선택이다.

태그: singleton-pattern java-reflection thread-safety enum-singleton double-checked-locking

6월 7일 19:32에 게시됨