1. 싱글톤 패턴이란?
싱글톤 패턴은 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 생성 패턴이다.
애플리케이션 전역에서 동일한 인스턴스를 공유해야 할 때 사용하며, 설정 관리자, 로거, 커넥션 풀 같은 경우가 대표적인 예시이다.
싱글톤 패턴의 핵심 요소
- private 생성자: 외부에서 직접 인스턴스를 생성하지 못하도록 막는다.
- private static 인스턴스 변수: 클래스 내부에서 유일한 인스턴스를 보관한다.
- 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 |
|---|