Singletone

허세진·2026년 1월 28일

backend

목록 보기
13/20

Singleton

싱글톤(Singleton) 패턴은 어떤 클래스의 인스턴스를 하나만 생성하고, 그 인스턴스에 전역적으로 접근할 수 있도록 하는 디자인 패턴이다.

싱글톤 패턴을 사용하는 이유

싱글톤을 사용하면 설정 정보, 로그 관리자, 캐시 관리자, 데이터베이스 커넥션 풀처럼 여러 개가 존재할 경우 시스템의 일관성이 깨지는 객체를 안전하게 관리할 수 있다.

싱글톤은 생성 비용이 큰 리소스의 중복 생성을 방지한다. 커넥션 풀이나 스레드 풀처럼 시스템 자원을 많이 사용하는 객체를 하나만 생성해 공유함으로써 성능 저하와 자원 낭비를 막을 수 있다.

싱글톤 패턴의 장단점

장점

  • 유일한 인스턴스
    애플리케이션 내에서 하나의 인스턴스만 생성되어 동일한 객체를 전역에서 공유할 수 있다.

  • 상태 일관성 보장
    여러 인스턴스가 존재할 때 발생할 수 있는 상태 불일치 문제를 방지한다.

  • 리소스 절약
    생성 비용이 큰 객체를 중복 생성하지 않아 메모리 사용량과 초기화 비용을 줄일 수 있다.

  • 중앙 집중 관리
    설정, 로그, 캐시와 같은 공통 자원을 하나의 객체에서 통합 관리할 수 있다.

  • 지연 초기화 가능
    실제로 필요한 시점에 객체를 생성하여 애플리케이션 시작 비용을 최소화할 수 있다.

단점

  • 강한 결합도
    전역 접근이 가능해지면서 싱글톤 객체에 대한 의존성이 코드 전반에 퍼질 수 있다.

  • 테스트 어려움
    하나의 인스턴스를 공유하기 때문에 Mock 대체가 어렵고 테스트 간 간섭이 발생할 수 있다.

  • 상태 공유 위험
    싱글톤이 상태를 가지는 경우 예기치 않은 사이드 이펙트가 발생할 수 있다.

  • 동시성 문제
    멀티스레드 환경에서 상태 변경 시 적절한 동기화가 없으면 Race Condition이 발생할 수 있다.

  • 설계 유연성 저하
    상속이나 구현 교체가 어렵고 확장 가능한 구조를 만들기 힘들다.

싱글톤 구현 방법

1. Eager Initialization

Eager Initialization은 클래스가 로딩되는 시점에 인스턴스를 미리 생성하는 방식이다.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

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

이 방법은 구현이 간단하고 동기화 비용이 없다. 하지만 실제로 사용하지 않아도 인스턴스가 생성된다. (초기화 비용이 큰 객체에는 부적합)

그래서

인스턴스 생성 비용이 작을 때, 반드시 사용될 객체일 때 이 방법 적합하다.

2. Lazy Initialization

Lazy Initialization는 인스턴스가 실제로 필요해지는 시점에 생성하는 방식이다.

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

위 코드는 필요할 때만 인스턴스를 생성해서 좋은것 같지만, 멀티스레드 환경에서 안전하지 않다. (동시에 접근하면 인스턴스가 여러 개 생성될 수 있음)

그래서

싱글톤 의미 자체가 깨져버려서 실무에서는 사용할 수 없는 방법이다.

3. Thread-Safe Lazy Initialization (synchronized 방식)

Thread-Safe Lazy Initialization은 지연 초기화 방식에 synchronized를 추가해서 동시 접근을 제어하는 방식이다.

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

이 방법은 구현이 비교적 간다하고 스레드 안전성을 확보할 수 있다. 하지만 메서드 호출마다 락을 획득해서 인스턴스 생성 이후에도 동기화 비용 발생한다. (성능 저하 가능성 큼)

그래서

정확하지만 비효율적인 방법이라고 할 수 있다.

4. Double-Checked Locking (DCL, 이중 검사 락)

Double-Checked Locking은 동기화 범위를 최소화해서 성능과 스레드 안전성을 모두 고려한 방식이다.

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

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

이 방법은 지연 초기화, 성능 최적화, 스레드 안전 등을 모두 확보한 방법이다. 단점은 코드 복잡도 높고, 메모리 모델 이해가 필요하다.

5. Initialization-on-Demand Holder Idiom

Initialization-on-Demand Holder Idiom은 클래스 로딩 특성을 활용한 지연 초기화 방식이다.

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

이 방법은 Singleton 클래스 로딩 시 인스턴스를 생성하는게 아니라 Holder 클래스가 처음 참조될 때 인스턴스를 생성한다.

Lazy Initialization, Thread-safe, 동기화 비용 X, 코드 간결 등의 이유로 가장 권장되는 구현 방식이다.

Enum Singleton 방식

Enum Singleton 방식은 Java의 enum 특성을 이용한 싱글톤 구현 방식이다.

public enum Singleton {
    INSTANCE;
}

이 방법은 Thread-safe 자동 보장, 직렬화 안전 등의 장점이 있고, Lazy Initialization 제어 불가라는 단점이 있다.

싱글톤 구현 시 주의사항

멀티스레드 환경

생성 시점뿐 아니라 상태 변경 시 동기화가 필요하다. (상태 없는 구조가 가장 안전)

테스트 어려움

Mock 교체 어려움, 테스트 간 상태 공유 위험, 병렬 테스트 취약 등을 고려해야 한다.

전역 상태의 위험성

어디서든 접근 할 수 있고, 변경 추적이 어렵고, 유지보수 비용 증가하는 것들을 고려해야 한다.

profile
로그를 파고드는 시간을 즐기는 백엔드 개발자, 허세진입니다.

0개의 댓글