본문 바로가기

Architecture/Design Pattern

[Design Pattern] 싱글톤 (Singleton) 패턴이란?

1. 싱글톤 패턴이란?

싱글톤 패턴은 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 생성 패턴이다.

 

애플리케이션 전역에서 동일한 인스턴스를 공유해야 할 때 사용하며, 설정 관리자, 로거, 커넥션 풀 같은 경우가 대표적인 예시이다.

 

싱글톤 패턴의 핵심 요소

  1. private 생성자: 외부에서 직접 인스턴스를 생성하지 못하도록 막는다.
  2. private static 인스턴스 변수: 클래스 내부에서 유일한 인스턴스를 보관한다.
  3. public static 접근 메서드: 외부에서 인스턴스에 접근할 수 있는 전역 접근점을 제공한다.

 

 

 

2. 싱글톤 패턴 구현 방식

싱글톤 패턴은 여러 방식으로 구현할 수 있으며, 각 방식마다 장단점과 적용 시나리오가 다르다.

 

2.1 Lazy Initialization (지연 초기화)

가장 기본적인 싱글톤 구현 방식이다.

public class LazyInitializationSettings {
    private static LazyInitializationSettings instance;

    private LazyInitializationSettings() {}

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

장점:

  • 인스턴스가 실제로 필요할 때 생성되므로 메모리를 절약할 수 있다.

단점:

  • 멀티스레드 환경에서 동시에 getInstance()를 호출하면 여러 인스턴스가 생성될 수 있다.
  • 스레드 안전성이 보장되지 않는다.

사용 시나리오: 단일 스레드 환경이거나 스레드 안전성이 크게 중요하지 않은 경우

 

 

2.2 Synchronized Method

메서드에 synchronized 키워드를 추가해 스레드 안전성을 확보한 방식이다.

public class SynchronizedInitializationSettings {
    private static SynchronizedInitializationSettings instance;

    private SynchronizedInitializationSettings() {}

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

장점:

  • 스레드 안전성이 보장된다.

단점:

  • 매번 getInstance() 호출 시 동기화 비용이 발생한다.
  • 인스턴스가 이미 생성된 후에도 불필요한 락 오버헤드가 계속 발생하므로 성능상 비효율적이다.

사용 시나리오: 스레드 안전성이 필요하지만 성능이 중요하지 않은 경우

 
 

2.3 Eager Initialization (즉시 초기화)

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

public class EagerInitializationSettings {
    private static final EagerInitializationSettings instance = new EagerInitializationSettings();

    private EagerInitializationSettings() {}

    public static EagerInitializationSettings getInstance() {
        return instance;
    }
}
 

장점:

  • 구현이 간단
  • 스레드 안전성이 보장
  • 클래스 로딩 시점에 JVM이 인스턴스 생성을 보장하므로 추가적인 동기화 메커니즘이 필요 없다.

단점:

  • 인스턴스를 사용하지 않더라도 클래스 로딩 시 무조건 생성되므로 메모리 낭비가 발생할 수 있다.
  • 생성 비용이 큰 객체의 경우 애플리케이션 시작 시간이 늘어날 수 있다.

사용 시나리오: 인스턴스가 반드시 필요하고 생성 비용이 크지 않은 경우

 

 

2.4. Double-Checked Locking (이중 확인 잠금)

동기화 비용을 최소화하면서 스레드 안전성을 확보한 방식이다.

public class DoubleCheckedLockingSettings {
    private static volatile DoubleCheckedLockingSettings instance;

    private DoubleCheckedLockingSettings() {}

    public static DoubleCheckedLockingSettings getInstance() {
        if (instance == null) { // 첫 번째 검사 (락 없이)
            synchronized (DoubleCheckedLockingSettings.class) { // 최초 생성 시에만 진입
                if(instance == null) { // 두 번째 검사 (락 안에서)
                    instance = new DoubleCheckedLockingSettings();
                }
            }
        }
        return instance;
    }
}

장점:

  • 인스턴스가 생성된 후에는 동기화 비용이 발생하지 않아 성능이 우수하다.
  • 지연 초기화의 장점과 스레드 안전성을 모두 확보한다.

주의사항:

  • volatile 키워드가 반드시 필요하다.
  • volatile이 없으면 인스턴스 생성 과정에서 명령어 재배치로 인해 완전히 초기화되지 않은 객체가 반환될 수 있다.
  • volatile 키워드를 지원하는 Java 5 이상에서만 제대로 동작한다.

사용 시나리오: 고성능이 필요하면서도 지연 초기화가 필요한 경우

 

 

2.5 Lazy Holder (권장 방식)

Bill Pugh가 제안한 방식으로, JVM의 클래스 로딩 메커니즘을 활용한 가장 세련된 구현이다.

public class LazyHolderSettings {
    private LazyHolderSettings() {}

    private static class SettingsHolder {
        private static final LazyHolderSettings INSTANCE = new LazyHolderSettings();
    }

    public static LazyHolderSettings getInstance() {
        return SettingsHolder.INSTANCE;
    }
}

장점:

  • getInstance()가 호출될 때 내부 정적 클래스가 로딩되면서 인스턴스가 생성된다 (지연 초기화)
  • JVM의 클래스 초기화 과정이 스레드 안전성을 보장하므로 별도의 동기화 코드가 필요 없다
  • 성능상 오버헤드가 전혀 없다
  • 코드가 간결하고 이해하기 쉽다

단점:

  • 대부분의 상황에서 가장 권장되는 구현 방식이다.
  • 리플렉션 API, 직렬화/역직렬화 상황에서 싱글톤이 깨질 수 있다.

사용 시나리오: 일반적인 대부분의 싱글톤 구현에 적합

 

 

2.6. Enum (가장 안전한 방식)

Joshua Bloch가 Effective Java에서 제안한 방식이다.

public enum EnumSettings {
    INSTANCE;
}

장점:

  • 직렬화/역직렬화 상황에서도 싱글톤이 보장됩니다
  • 리플렉션 공격에 안전합니다
  • 구현이 가장 간단합니다
  • 스레드 안전성이 보장됩니다

단점:

  • Enum은 상속이 불가능합니다
  • 지연 초기화가 불가능합니다 (클래스 로딩 시 즉시 생성)
  • 싱글톤 외의 다른 용도로 확장하기 어렵습니다

사용 시나리오: 직렬화가 필요하거나 보안이 중요한 경우

 

구현 방식 지연 초기화 스레드 안전 성능 복잡도 추천도
Lazy Initialization O X 높음 낮음 ★☆☆☆☆
Synchronized O O 낮음 낮음 ★★☆☆☆
Eager Initialization X O 높음 낮음 ★★★☆☆
Double-Checked Locking O O 높음 높음 ★★★☆☆
Lazy Holder O O 높음 중간 ★★★★★
Enum X O 높음 낮음 ★★★★☆

 

 

 

3. 실무에서의 싱글톤 패턴 활

3.1. Java 표준 라이브러리 - Runtime 클래스

구현 방식: Eager Initialization

소스 코드 위치: java.lang.Runtime

public final class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    private Runtime() {}

    public static Runtime getRuntime() {
        return currentRuntime;
    }
    
    // JVM 관련 메서드들...
}

선택 이유: Runtime은 JVM의 핵심 기능을 담당하므로 애플리케이션 시작 시 무조건 필요하다. 지연 초기화의 이점이 없으므로 간단하고 확실한 Eager Initialization을 사용한다.

 

 

3.2. Spring Framework - Bean 관리

구현 방식: 기본 Eager Initialization (설정으로 Lazy 가능)

Spring은 기본적으로 모든 싱글톤 Bean을 ApplicationContext 시작 시 즉시 생성한.

 

내부 구현 (DefaultSingletonBeanRegistry):

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry 
        implements SingletonBeanRegistry {

    // Bean 이름 -> 인스턴스를 저장하는 Map
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    // 싱글톤으로 등록된 Bean의 이름들
    private final Set<String> registeredSingletons = 
            Collections.synchronizedSet(new LinkedHashSet<>(256));

    @Override
    public void registerSingleton(String beanName, Object singletonObject) 
            throws IllegalStateException {
        Assert.notNull(beanName, "Bean name must not be null");
        Assert.notNull(singletonObject, "Singleton object must not be null");
        
        synchronized (this.singletonObjects) {
            Object oldObject = this.singletonObjects.get(beanName);
            if (oldObject != null) {
                throw new IllegalStateException(
                    "Could not register object [" + singletonObject + 
                    "] under bean name '" + beanName + 
                    "': there is already object [" + oldObject + "] bound");
            }
            addSingleton(beanName, singletonObject);
        }
    }

    protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            this.singletonObjects.put(beanName, singletonObject);
            this.registeredSingletons.add(beanName);
        }
    }

    @Override
    public Object getSingleton(String beanName) {
        return this.singletonObjects.get(beanName);
    }
}
 

선택 이유:

  • Eager (기본값): 애플리케이션 시작 시 설정 오류를 즉시 발견할 수 있어 안전하다
  • Lazy (선택적): 무거운 초기화 작업이 필요하거나 특정 환경에서만 사용되는 Bean에 적용

Spring의 싱글톤 특징:

  • 클래스 자체가 인스턴스를 관리하는 것이 아니라 Spring 컨테이너(Registry)가 Map으로 관리
  • Bean 이름을 Key로 사용해 여러 종류의 싱글톤을 하나의 Registry에서 관리
  • synchronized 블록으로 스레드 안전성 확보
  • JVM 전체가 아닌 Spring Container당 하나의 인스턴스

 

 

3.3. 데이터베이스 커넥션 풀

Spring Boot + JPA 환경 (자동 설정)

Spring Boot 2.0부터는 JPA를 사용하면 HikariCP가 자동으로 싱글톤 Bean으로 등록된다.

 

의존성 (HikariCP 자동 포함)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

 

자동 설정 원리:

Spring Boot의 DataSourceConfiguration 클래스가 자동으로 커넥션 풀을 Bean으로 생성한다.

// org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration

@Configuration
@ConditionalOnClass({HikariDataSource.class})
@ConditionalOnProperty(
    name = {"spring.datasource.type"},
    havingValue = "com.zaxxer.hikari.HikariDataSource",
    matchIfMissing = true  // 설정이 없으면 자동으로 HikariCP 사용
)
static class Hikari extends DataSourceConfiguration {
    
    @Bean  // 기본적으로 싱글톤 스코프
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return createDataSource(properties, HikariDataSource.class);
    }
}

 

 

 

4. 마무리

구현 방식 선택 가이드

  • 일반적인 경우: Lazy Holder 방식을 사용하자. 성능, 안전성, 코드 간결성 모두 우수.
  • 직렬화가 필요한 경우: Enum 방식을 사용하자. 별도의 readResolve() 구현 없이도 싱글톤이 보장된다.
  • 즉시 초기화해도 문제없는 경우: Eager Initialization을 사용하자. 가장 간단하고 명확하다.
  • Spring 환경: Spring이 Bean을 싱글톤으로 자동 관리하므로 별도의 싱글톤 패턴 구현이 필요 없다. @Component, @Service 등의 어노테이션만 사용하면 된다.

 

싱글톤 패턴 사용 시 주의사항

1. 멀티스레드 환경: 상태를 갖는 싱글톤은 반드시 스레드 안전하게 구현해야 한다.

// 나쁜 예
@Component
public class Counter {
    private int count = 0;  // 여러 스레드가 동시 접근 가능
    
    public void increment() {
        count++;  // Thread-safe하지 않음
    }
}

// 좋은 예
@Component
public class Counter {
    private final AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Thread-safe
    }
}

 

멀티스레드 환경에서 싱글톤 사용은 따로 게시글을 하나 더 작성할 예정이다!

 

 

2. 테스트 어려움: 싱글톤은 전역 상태를 가지므로 단위 테스트가 어려울 수 있다. Spring의 의존성 주입을 활용하면 Mock 객체로 대체하기 쉽다.

 

3. 남용 방지: 정말로 전역 상태가 필요한지, 의존성 주입으로 해결할 수 있는 문제는 아닌지 신중히 검토해야 한다.

 

'Architecture > Design Pattern' 카테고리의 다른 글

[Design Pattern] 전략 (Strategy) 패턴이란?  (0) 2025.11.28