싱글톤은 디자인 패턴 중 하나로, 클래스의 인스턴스를 단 하나만 생성하여 보장하고, 어디서든 그 인스턴스를 접근할 수 있도록 하는 디자인 패턴이다.
싱글톤 패턴은 애플리케이션 전체에서 하나의 인스턴스만 유지하기 때문에 불필요한 객체 생성을 줄이고 메모리 사용을 효율적으로 관리할 수 있다. 또한 생성 비용이 큰 객체를 반복적으로 생성하지 않아도 되므로 성능 측면에서도 유리하다. 그래서 DB 커넥션 관리, 설정 객체, 캐시 관리자와 같이 전역적으로 공유되어야 하는 컴포넌트에 주로 사용된다.
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
싱글톤 패턴은 기본 생성자의 접근 제어자를 private로 설정하여 외부에서 직접 객체를 생성하지 못하도록 제한한다. 클래스 내부에서 인스턴스를 관리하고, getInstance()를 통해서만 객체에 접근하도록 한다.
다음은 싱글톤 패턴을 구현 하는 다양한 방법이다. 많은 방법들이 있지만 각각의 장단점을 알고 상황에 맞게 쓰는 것이 중요하다.
위와 같이 여러 구현 방식이 있으며, 이 중 몇 가지를 구현 패턴의 특징과 차이점을 살펴보자.
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
위 코드처럼 객체를 생성하는 시점이 아닌 getInstance()를 호출한 시점에 instance가 없다면 생성하는 방식이다. 이 방식은 필요한 시점에 instance를 생성한다는 장점이 있지만, 멀티 스레드 환경에서 동기화 문제가 발생할 수 있다.
class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Double-Checked Locking 방식은 멀티 스레드 환경에서도 안전하게 동작하며, 메소드 전체에 동기화를 적용하는 대신 인스턴스 생성 시점에만 최소한의 동기화를 수행하기 때문에 불필요한 Lock 획득 과정을 줄여 성능 저하를 최소화할 수 있다.
하지만, 이 방식은 구현 복잡성이 증가하고 volatile이나 synchronized에 대한 이해가 부족하다면 예상하지 못한 동시성 문제를 유발할 수 있으며, 코드의 가독성과 유지보수성이 떨어질 수 있다는 단점이 있다.
class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
Static Holder 패턴은 JVM의 클래스 로딩 메커니즘을 활용하여 Lazy initialization과 Thread-Safety를 만족시키는 구현 기법이다.
inner 클래스는 해당 클래스가 실제 사용되는 시점에 로딩된다. 즉, getInstance()가 호출되기 전까지는 인스턴스가 생성되지 않는다. 또한, JVM은 클래스 로딩 시 thread safe가 보장되기 때문에 멀티 스레드 환경에서도 안전하게 동작한다.