개요
스프링 강의를 듣던 중 싱글톤에 대한 이야기가 나왔다. 싱글톤을 한 번 공부해 보자.
정의
싱글톤 패턴은 디자인 패턴 중 하나로, 특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴이다.
특징
싱글톤의 특징은 다음과 같다.
- 인스턴스의 유일성을 보장
- 전역적으로 접근 가능
구현방법
싱글톤의 구현 방법은 여러 가지가 있다.
1. Eager Initialization
Eager Initialization은 가장 간단한 형태의 구현 방법이다. 싱글톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법이다. 해당 인스턴스를 사용하지 않더라도 인스턴스를 생성하기 때문에 낭비가 발생할 수 있다.
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() { } // private 생성자를 통해 new Singleton()을 막아준다.
public static Singleton getInstance() {
return instance;
}
}
2. Static Block Initialization
1번의 Eager Initialization과 비슷하지만 static block을 통해서 예외 처리에 대한 옵션을 제공한다.
public class Singleton {
private static Singleton instance;
private Singleton() { } // private 생성자로 new Singleton()을 막는다.
static {
try{
instance = new Singleton();
} catch (Exception e){
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
public static Singleton getInstance(){
return instance;
}
}
위와 같이 구현할 경우 싱글톤 클래스의 인스턴스를 생성할 때 발생할 수 있는 예외에 대한 처리를 할 수 있지만, Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하기 때문에 여전히 큰 리소스를 다루는 경우에는 적합하지 않다.
3. Lazy Initialization
이름에 걸맞게, 나중에 초기화하는 방법이다. 다른 곳에서 getInstance()를 호출할 때 싱글톤 인스턴스가 생성된다.
public class Singleton {
private static Singleton instance;
private Singleton() { } // private 생성자로 new Singleton()을 막는다.
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
위와 같이 만든다면 앞선 1,2번 방식의 문제(인스턴스 낭비)를 보완할 수 있다. 하지만 이 방식은 멀티 쓰레드 환경에서 동기화 문제가 있다. 만약, 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance()를 호출한다면 데이터 레이스가 발생해 예상치 못한 결과를 얻을 수 있고, 단 하나의 인스턴스를 생성한다는 싱글톤 패턴에 위반하는 문제점이 생길 수 있다. Lazy Initialization 방식은 싱글 쓰레드 환경이 보장됐을 때 사용해야 한다.
4. Thread Safe Singleton
Lazy Initialization에서 멀티 쓰레드 문제를 해결하기 위한 방식으로 getInstance() 메소드에 synchronized를 걸어두는 방식이다. 하지만 getInstance에 synchronized를 걸어두면, 비용이 크기 때문에 자주 호출하는 상황에서는 성능이 하락된다. 이를 해결하기 위해 double checked locking 방식을 사용하면 된다. getInstance() 메서드에 락을 걸지 않고 instance가 null일 경우에만 synchronized가 작동하도록 한다.
public class Singleton {
private static Singleton instance;
private Singleton() { } // private 생성자로 new Singleton()을 막는다.
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
5. Bill Pugh Singleton Implementation
Bill Pugh라는 사람이 고안한 방식으로, inner static helper class를 사용하는 방식이다. 앞선 방식들이 안고 있는 문제점들을 대부분 해결한 방식으로, 현재 가장 널리 쓰이는 싱글톤 구현 방법이다.
public class Singleton {
private Singleton() { } // private 생성자로 new Singleton()을 막는다.
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
private static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
SingletonHelper 클래스는 Singleton 클래스가 로드될 때에도 로드되지 않다 getInstance()가 호출됐을 때 JVM 메모리에 로드되고, 인스턴스를 생성하게 된다. synchronized를 사용하지 않기에 멀티쓰레딩환경 성능 저하 문제 또한 해결된다.
6. Enum Singleton
앞선 싱글톤 방식은 사실 완전히 안전할 수 없다. 왜냐하면, Java의 Reflection을 통해서 싱글톤을 파괴할 수 있기 때문이다. Joshua Bloch는 Enum으로 싱글톤을 구현하는 방법을 제한했지만, 1, 2번과 같이 사용하지 않는 경우의 메모리 문제를 해결하지 못한 것과 유연성이 떨어진다는 면에서 한계를 가지고 있다.
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
// do Something
}
}
이런 구현법이 있다는 것만 알아두자.
단점
싱글톤의 장점말고 단점에 대해 알아보자.
단점은 다음과 같다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 유연성이 떨어진다.
- 안티패턴으로 불리기도 한다.
안티패턴
디자인 패턴은 프로그래밍을 하면서 상황에 따라 자주 쓰이는 설계방법을 패턴화 시킨 것이다. 안티 패턴은 자주 사용되는 프로그래밍 패턴이지만, 잘 모르고 사용한다면 정말 커다란 장애를 만들 수 있는 패턴을 뜻한다.
정리
- 싱글톤은 객체의 유일성을 보장하는 디자인 패턴
- 전역적인 접근점을 제공
- 구현 방법이 다양하지만 Bill Pugh Singleton Implementation 방식을 주로 많이 사용