싱글톤(Singleton) 패턴은 어떤 클래스의 인스턴스를 하나만 생성하고, 그 인스턴스에 전역적으로 접근할 수 있도록 하는 디자인 패턴이다.
싱글톤을 사용하면 설정 정보, 로그 관리자, 캐시 관리자, 데이터베이스 커넥션 풀처럼 여러 개가 존재할 경우 시스템의 일관성이 깨지는 객체를 안전하게 관리할 수 있다.
싱글톤은 생성 비용이 큰 리소스의 중복 생성을 방지한다. 커넥션 풀이나 스레드 풀처럼 시스템 자원을 많이 사용하는 객체를 하나만 생성해 공유함으로써 성능 저하와 자원 낭비를 막을 수 있다.
유일한 인스턴스
애플리케이션 내에서 하나의 인스턴스만 생성되어 동일한 객체를 전역에서 공유할 수 있다.
상태 일관성 보장
여러 인스턴스가 존재할 때 발생할 수 있는 상태 불일치 문제를 방지한다.
리소스 절약
생성 비용이 큰 객체를 중복 생성하지 않아 메모리 사용량과 초기화 비용을 줄일 수 있다.
중앙 집중 관리
설정, 로그, 캐시와 같은 공통 자원을 하나의 객체에서 통합 관리할 수 있다.
지연 초기화 가능
실제로 필요한 시점에 객체를 생성하여 애플리케이션 시작 비용을 최소화할 수 있다.
강한 결합도
전역 접근이 가능해지면서 싱글톤 객체에 대한 의존성이 코드 전반에 퍼질 수 있다.
테스트 어려움
하나의 인스턴스를 공유하기 때문에 Mock 대체가 어렵고 테스트 간 간섭이 발생할 수 있다.
상태 공유 위험
싱글톤이 상태를 가지는 경우 예기치 않은 사이드 이펙트가 발생할 수 있다.
동시성 문제
멀티스레드 환경에서 상태 변경 시 적절한 동기화가 없으면 Race Condition이 발생할 수 있다.
설계 유연성 저하
상속이나 구현 교체가 어렵고 확장 가능한 구조를 만들기 힘들다.
Eager Initialization은 클래스가 로딩되는 시점에 인스턴스를 미리 생성하는 방식이다.
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
이 방법은 구현이 간단하고 동기화 비용이 없다. 하지만 실제로 사용하지 않아도 인스턴스가 생성된다. (초기화 비용이 큰 객체에는 부적합)
그래서
인스턴스 생성 비용이 작을 때, 반드시 사용될 객체일 때 이 방법 적합하다.
Lazy Initialization는 인스턴스가 실제로 필요해지는 시점에 생성하는 방식이다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
위 코드는 필요할 때만 인스턴스를 생성해서 좋은것 같지만, 멀티스레드 환경에서 안전하지 않다. (동시에 접근하면 인스턴스가 여러 개 생성될 수 있음)
그래서
싱글톤 의미 자체가 깨져버려서 실무에서는 사용할 수 없는 방법이다.
Thread-Safe Lazy Initialization은 지연 초기화 방식에 synchronized를 추가해서 동시 접근을 제어하는 방식이다.
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
이 방법은 구현이 비교적 간다하고 스레드 안전성을 확보할 수 있다. 하지만 메서드 호출마다 락을 획득해서 인스턴스 생성 이후에도 동기화 비용 발생한다. (성능 저하 가능성 큼)
그래서
정확하지만 비효율적인 방법이라고 할 수 있다.
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;
}
}
이 방법은 지연 초기화, 성능 최적화, 스레드 안전 등을 모두 확보한 방법이다. 단점은 코드 복잡도 높고, 메모리 모델 이해가 필요하다.
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 방식은 Java의 enum 특성을 이용한 싱글톤 구현 방식이다.
public enum Singleton {
INSTANCE;
}
이 방법은 Thread-safe 자동 보장, 직렬화 안전 등의 장점이 있고, Lazy Initialization 제어 불가라는 단점이 있다.
생성 시점뿐 아니라 상태 변경 시 동기화가 필요하다. (상태 없는 구조가 가장 안전)
Mock 교체 어려움, 테스트 간 상태 공유 위험, 병렬 테스트 취약 등을 고려해야 한다.
어디서든 접근 할 수 있고, 변경 추적이 어렵고, 유지보수 비용 증가하는 것들을 고려해야 한다.