프로그래밍/디자인패턴

싱글톤 패턴(Singleton Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 17:30

애플리케이션을 개발하다 보면 전역적으로 단 하나의 인스턴스만 존재해야 하는 객체들이 있습니다. 설정 파일 관리자, 커넥션 풀, 로그 기록 객체 등이 대표적입니다.

이를 위해 사용하는 싱글톤 패턴은 개념은 간단하지만, "멀티스레드 환경에서 안전한가?" 라는 질문이 들어오면 구현 방법이 매우 다양해집니다.

오늘은 싱글톤 패턴이 진화해 온 과정과, 실무에서 써야 할 가장 완벽한 구현 방법(Bill Pugh, Enum)까지 총 5가지를 정리해 봅니다.


1. 이른 초기화 (Eager Initialization)

가장 단순하고 안전한 방법입니다. 클래스가 로딩되는 시점에 인스턴스를 미리 만들어버리는 방식입니다.

코드 예시

Java
public class EagerSingleton {
    // 클래스 로딩 시 바로 생성 (Thread-safe)
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {} // 생성자 막기

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

장단점

  • 장점: 구현이 쉽고, 스레드 안전성(Thread-safe)이 보장됩니다.
  • 단점: 사용하지 않더라도 메모리를 점유합니다. 인스턴스 생성 비용이 크다면 비효율적입니다.

2. 게으른 초기화 (Lazy Initialization) - 위험!

메모리 낭비를 막기 위해, 실제 getInstance()가 호출될 때 인스턴스를 생성하는 방식입니다. 하지만 치명적인 문제가 있습니다.

코드 예시

Java
public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        // 멀티스레드 환경에서 위험!
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

치명적 문제점

  • Race Condition: 스레드 A와 B가 동시에 if (instance == null)을 통과하면, 인스턴스가 두 개 생성되어 싱글톤이 깨집니다. 실무 사용 금지.

3. DCL (Double-Checked Locking)

위의 멀티스레드 문제를 해결하기 위해 synchronized를 적용한 방식입니다. 성능 저하를 막기 위해 인스턴스가 없을 때만 동기화를 겁니다.

코드 예시

Java
public class DclSingleton {
    // volatile 키워드 필수 (CPU 캐시 문제 방지)
    private static volatile DclSingleton instance;

    private DclSingleton() {}

    public static DclSingleton getInstance() {
        if (instance == null) {
            synchronized (DclSingleton.class) {
                if (instance == null) {
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }
}

장단점

  • 장점: 필요한 시점에 생성하며 스레드 안전성도 챙겼습니다.
  • 단점: 코드가 복잡하고, JDK 1.5 이전 버전에서는 volatile 관련 이슈가 있었습니다. (현재는 잘 동작하지만 코드가 지저분합니다.)

4. Holder Idiom (Bill Pugh Solution) - 추천!

가장 권장되는 일반적인 방식입니다. synchronized 없이도 자바의 클래스 로더 메커니즘을 이용해 스레드 안전성을 보장합니다.

코드 예시

Java
public class HolderSingleton {
    private HolderSingleton() {}

    // 내부 스태틱 클래스 (지연 로딩됨)
    private static class SingletonHolder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

작동 원리 (Lazy Loading)

  1. HolderSingleton 클래스가 로드되어도 SingletonHolder는 로드되지 않습니다.
  2. getInstance()가 호출되는 순간 SingletonHolder가 로드되며 인스턴스가 생성됩니다.
  3. 클래스 초기화는 JVM이 보장하는 원자적(Atomic) 작업이므로 Thread-safe 합니다.

5. Enum Singleton - 가장 안전함

Joshua Bloch(이펙티브 자바 저자)가 추천하는 방식입니다.

코드 예시

Java
public enum EnumSingleton {
    INSTANCE; // 이게 끝입니다.

    public void doSomething() {
        System.out.println("싱글톤 로직 수행");
    }
}

장점

  • 가장 완벽한 보안: 리플렉션(Reflection) 공격이나 직렬화(Serialization) 역직렬화 시에도 싱글톤이 깨지지 않도록 JVM이 보장합니다.
  • 간결함: 코드가 가장 짧습니다.

번외: Spring Bean은 싱글톤인가요?

엄밀히 말하면 자바 언어 차원의 싱글톤 패턴과는 다릅니다.

  • 자바 싱글톤: 클래스 로더당 1개 객체 보장.
  • 스프링 빈: 스프링 컨테이너(ApplicationContext)당 1개 빈 보장.

스프링 프레임워크를 쓴다면 직접 싱글톤을 구현하기보다 **스프링 컨테이너에게 객체 관리를 위임(@Component, @Bean)**하는 것이 가장 좋습니다. 하지만 프레임워크 없는 순수 자바 개발 시에는 위 패턴들이 필수적입니다.


요약: 어떤 방식을 선택해야 할까?

방식 설명 추천 상황
Eager 처음부터 생성 인스턴스가 가볍고 반드시 쓰일 때
Lazy if문 체크 절대 사용 금지 (멀티스레드 버그)
DCL if + synchronized 레거시 코드에서나 보임. 굳이 쓸 필요 없음
Holder 내부 클래스 사용 일반적인 자바 프로젝트 (Best 1)
Enum Enum 타입 가장 안전한 방식 (Best 2)

결론

순수 자바 코드에서 싱글톤을 구현해야 한다면 고민하지 말고 4번(Holder) 또는 5번(Enum) 방식을 사용하세요. 복잡한 동기화 코드 없이도 성능과 안전성을 모두 잡을 수 있습니다.