[디자인 패턴] 5. 싱글턴 패턴(Singleton Pattern)
이 글은 헤드퍼스트 디자인패턴과 GoF 디자인패턴을 읽고 정리한 글입니다.
1. 싱글턴 패턴(Singleton Pattern)
1.1. 구성 요소
Singleton
- Singleton 객체를 가져올 수 있는 방법을 제공합니다.
- 단 하나의 유일한 인스턴스만을 갖도록 하는 책임을 갖습니다.
1.2. 적용 방법
public class Singleton {
private static Singleton instance; // (1)
private Singleton() { // (4)
}
public static Singleton getInstance() { // (2)
if (instance == null) { // (3)
instance = new Singleton();
}
return instance;
}
}
- 싱글턴으로 관리할 클래스에 private static 필드를 추가합니다.
- 싱글턴 인스턴스를 가져오기 위한 public static 메서드(getInstance)를 선언합니다.
- getInstance 내에서 지연된 초기화를 구현합니다.
- 클래스 생성자를 private 으로 만들어 외부에서 호출하지 못하게 합니다.
- 클라이언트 코드 중 싱글턴의 생성자에 대한 모든 호출을 Singleton.getInstance() 로 교체합니다.
1.3. 적용 시기
- 모든 클라이언트가 사용할 수 있는 단일 인스턴스만 있어야 할 때 싱글턴 패턴을 사용할 수 있습니다.
- 또한 클라이언트 코드의 변경 없이 단일 인스턴스에 대하여 subclassing을 통해 기능을 확장하고 싶다면 싱글턴 패턴을 사용하여 적용할 수 있습니다.
1.4. 정리
싱글턴 패턴을 사용하면 하나의 인스턴스를 갖는다는 일관성을 클래스 단에서 쉽게 제어할 수 있습니다. 또한 그 인스턴스에 대하여 전역 접근 지점을 클라이언트들에게 제공할 수 있습니다.
싱글턴 패턴은 항상 전역 변수와 많이 비교되긴 합니다. 전역 변수를 사용하여 싱글턴 패턴과 같이 사용할 수 있지만, 전역 변수는 해당 전역 변수는 유일성을 보장하는것이 어렵습니다. 다른 클라이언트에서 전역 변수가 정의되어 있는지 모른 채 새로 정의하는 경우 그 인스턴스의 유일성은 바로 깨져 버리기에, 유일성을 보장하는데는 싱글턴 패턴이 더 좋은 방법입니다. 또한 전역 변수와 달리 싱글턴 패턴은 subclassing을 통해 기능을 확장하고, 런타임에 설정을 변경할 수 있습니다. 이 부분은 구현 방법에서 더 자세히 다루도록 하겠습니다.
지금까지 싱글턴 패턴은 단 하나의 유일한 인스턴스를 갖도록 제어하기 위한 패턴으로 이야기 되었는데요, 실제로는 한개 이상의 인스턴스를 관리하도록 기능을 확장할 수 있습니다. getInstance() 메서드에서 구현만 달리하면 하나 이상의 인스턴스에 대해 제공하도록 할 수 있으며, 클라이언트 코드에는 변경이 없기에 이러한 변경은 매우 유연하게 적용할 수 있습니다.
2. 싱글턴 패턴 구현 방법 및 고려 사항
2.1. 단 하나의 유일한 인스턴스를 보장하라
단 하나의 유일한 인스턴스만을 갖도록 보장하는 방법은 생성자를 노출하지 않는 것입니다. getInstance() 메서드는 싱글턴 인스턴스에 대한 참조만 갖고있고 첫 호출 시 한번만 생성자를 통해 초기화하도록 합니다. 이는 lazy initialization으로 동작하게 됩니다.
위의 예시와 다르게 생성자를 protected 로 선언할 수 있습니다. 이러한 경우는 외부에서 생성자에 접근을 할 수 없음을 보장하며 싱글턴의 자식 클래스는 확장할 수 있는 기회를 갖도록 합니다.
멀티 스레드 환경에서 싱글턴 패턴의 getInstance()를 여러 스레드가 동시에 접근하면 객체를 여러번 생성할 수 있습니다. 이런 경우는 동기화 블럭을 추가하고 블럭 내에서 싱글턴 인스턴스의 초기화 여부를 확인하여 여러번 생성되는 문제를 방지할 수 있습니다. 이 방법을 DCL(Double-Checked Locking)이라고 부릅니다.
public 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;
}
}
2.2. 싱글턴 클래스의 Subclassing
싱글턴 패턴을 잘 적용하는 방법 중 하나는, 싱글턴 인스턴스 참조를 초기화 할 때 싱글턴 인스턴스의 하위 클래스로 초기화 하는 것입니다. 가장 쉬운 구현 방법은 getInstance() 에서 어떠한 인스턴스로 초기화 할 지 지정하는 것입니다.
다른 방법으로는 getInstance()의 구현을 하위 클래스가 가져가는 것입니다. 이렇게 구성한다면 실제 어떠한 인스턴스가 싱글턴 참조로 할당될지는 LinkTime 또는 RunTime에 결정되게 됩니다. LinkTime(Java 에서는 클래스로더에 의해 로드될 때)에 초기화 되는 경우 런타임에 자유롭게 싱글턴 인스턴스를 교체할 수 없다는 단점이 있지만, Runtime에 초기화하는 경우에는 실제 사용할 싱글턴 하위클래스를 모두 알고 소스에서 가지고 있어야 한다는 단점이 있습니다. 결국 두 방식 모두 충분치는 않습니다.
가장 유연한 방법으로는 싱글턴 레지스트리(registry of singleton)를 사용하는 것입니다. 싱글턴 클래스는 초기화 시 레지스트리에 자기 자신을 등록합니다. 등록 시 키 사용될 자신의 클래스 이름과 유일 인스턴스(참조)를 등록합니다. getInstance() 가 호출되면 클래스 이름을 통해 레지스트리로 부터 등록된 인스턴스를 반환받고 이를 클라이언트에 반환합니다. 레지스트리를 사용한다면 싱글턴으로 사용할 클래스가 유일한 인스턴스를 유지하기 위한 책임을 지지 않아도 되는 장점이 존재합니다. 유일 인스턴스로써 유지하는것의 책임은 싱글턴 레지스트리가 가지며, 자신이 싱글턴으로 관리되길 바라는 클래스는 초기화 시 레지스트리에 자신을 등록하기만 하면 됩니다.
싱글턴 레지스트리는 스프링 프레임워크에서 빈을 관리하는 IoC 컨테이너의 관리방식과 매우 유사하게 보입니다. @Container 애노테이션이 적용된 클래스를 빈으로 등록하여 싱글턴으로 관리하게 되는것이 레지스트리에 자신을 참조하는것과 대응됩니다. 또한 GoF 책의 예시 중 환경 변수에 따라 사용할 싱글턴 인스턴스를 결정하는 방법은 @Profile 애노테이션을 통해 특정 환경에서만 빈으로 등록하는 방법과 유사성이 존재하는것으로 보입니다. 스프링 프레임워크는 책에서 제시한 싱글턴 패턴 사용 방법으로 제시된 궁극의(?) 구현 방식을 모두 구현하고 있는것으로 보이는것이 신기하네요.