싱글턴 패턴 Singleton Pattern
싱글턴 Singleton 이란?
- 인스턴스를 오직 한개만 만들어 제공하는 클래스
- 인터페이스나 상속을 사용하지 못한다. 결합도가 높아질 가능성이 높다.
- 가능한 최소한의 사용을 권장한다.
- 아래는 싱글턴을 구현하는 다양한 방법이다.
스레드에 안전하지 않은 방식
- 아래의 방식은 기본적인 스레드 구현 방식이다.
- 정적 맴버가
- null일 경우 새로 생성하여 대입한다.
- null이 아닐 경우 정적 맴버를 리턴한다.
class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
sleep(200);
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
- 위의 방법은 멀티스레드로부터 안전하지 않다.
List<Singleton> list = new ArrayList<>();
ExecutorService es = Executors.newFixedThreadPool(10);
es.invokeAll(List.of(
(() -> list.add(Singleton.getInstance()))
, (() -> list.add(Singleton.getInstance()))
));
list.get(0) == list.get(1); // false
동기화
- 위의 메서드를 동기화한다. 단 하나의 스레드만 접근을 허용한다.
- 다만, 멀티스레드 상황에서 락이 발생할 수 있다.
public synchronized static Singleton getInstance() {
// 상동
}
volatile과 double check locking
- 각각의 스레드가 점유하는 cpu캐시를 참조하지 않고 메인 메모리를 참조한다. 이를 통해 변수가 불일치함을 방어한다.
- 쓰기 작업에 대한 안전한 작업을 위하여 synchronized와 함께 DCL(double check locking)을 적용한다.
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;
}
}
즉각 초기화와 final
- 현재까지의 구현은 복잡하고 길고 성능을 고민해야 하는 등 자바에 대한 많은 이해를 요구한다.
- 가장 단순하고 효과적인 방법을 선택한다.
- 맴버 변수에 인스턴스 선언과 동시에 초기화한다.
- final을 사용할 수 있다.
- 다만, 클래스 로딩 시 즉각 초기화 된다. 사용하지 않을 경우 메모리 낭비가 될 수 있다.
- final이므로 필드 자체를 public으로 공개할 수 있다. 하지만 내부 로직이 어떻게 변경될지 모르므로, 필드는 private으로 두고 정적 메서드로 리턴한다.
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
중첩 클래스와 지연로딩
- 중첩 클래스는 그것이 클래스임에도 불구하고 호출 될 때 로딩된다.
- 중첩 클래스에 static final 필드를 작성하여 유일함과 불변을 보장하는 싱글턴을 구현한다.
- 코드 작성이 쉽다. 지연로딩, final, thread-safety하다. 가장 선호하는 방식이다.
class Singleton {
private Singleton() {}
// 중첩 클래스는 호출할 때 로딩된다.
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
리플렉션과 역직렬화
- 리플렉션과 역직렬화를 활용하여 인스턴스를 유일하지 않도록 만들 수 있다.
- 리플렉션은 enum이 아닌 이상 방어할 수 없다. 하지만 직렬화 사용 시 readResolve 메서드를 아래와 같이 정의하여 방어할 수 있다.
class SingletonSafe implements Serializable {
protected Object readResolve(){
return SingletonHolder.INSTANCE;
}
}
SingletonSafe i1 = SingletonSafe.getInstance();
SingletonSafe i2 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
out.writeObject(i1);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
i2 = (SingletonSafe) in.readObject();
}
i1 == i2; // true
싱글턴을 우회하는 방법은 무엇이 있을까?
- 정적 메서드, 정적 맴버 변수
- 클래스 로더에 올라온 정적 메서드와 맴버 변수는 유일함을 보장 받는다.
- 다만, 인스턴스 맴버와 메서드를 참조할 수 없다. 인스턴스와 합성해야 할 경우 사용 불가능하다.
- enum
- 각 상수마다 인스턴스를 생성하며 유일함을 보장받는다.
- 역직렬화를 하더라도 동일성(유일함)을 보장받는다.
- 리플렉션으로부터 안전하다.
- 다만, enum의 한계를 가진다. 상속을 할 수 없다. 일반 인스턴스보다 유연하지 않다.
- 싱글턴을 포기하고 필요할 때마다 새로운 객체를 생성한다.
- 굳이 싱글턴을 할 필요가 없으면 사용할 때마다 객체를 생성한다.
- 앞서의 세 방식이 권장된다. 불가능할 경우 싱글턴을 고려한다.
싱글턴이 필요할 때는 언제일까?
- 모듈이나 어플리케이션에 유일함을 보장받아야 하는 객체. 설정 정보 등.
- 인스턴스 생성에 리소스를 많이 사용하여 캐싱처리가 필요한 경우.
- 하나의 객체로 재사용이 유리할 때.
- 스프링은 스프링 컨텍스트를 활용하여 복잡한 싱글턴 패턴 적용 없이 빈의 유일함을 보장한다. 빈 관리는 스프링의 가장 중요한 강점 중 하나이다.
참조
- 백기선, 디자인 패턴 강의(https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4)
- O’Reilly, “헤드 퍼스트 디자인 패턴”
- 조슈아블로크, “이펙티브 자바”
- 신용권, “이것이 자바다”