[Effective Java] Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다.
사실 만능인 것처럼 보이는 이 싱글턴은 클라이언트 테스트를 더 어렵게 만드는 안티 패턴이기도 합니다.
싱글턴 생성 방식
싱글턴을 생성하는 방식은 두 가지가 존재합니다.
1. public static final 필드 방식의 싱글턴
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
...
}
첫 번째 생성 방식에서 private 생성자는 INSTANCE 정적 인스턴스를 생성할 때 단 한 번만 실행이 됩니다.
첫 번째 생성 방식의 장점
1. 해당 클래스가 싱글턴임이 바로 드러난다.
2. 간결하다.
2. 정적 팩터리 방식의 싱글턴
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
...
}
Elvis.getInstance는 항상 INSTANCE의 참조를 반환하므로 또 다른 인스턴스는 결코 만들어지지 않습니다.
두 번째 생성 방식의 장점
1. API 변경 없이도 싱글턴이 아니게 변경할 수 있다.
2. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
3. 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다. (가령 Elvis::getInstance를 Supplier<Elvis>로 사용한다.)
이렇게 만든 싱글턴을 직렬화하려면,
위의 두 방식으로 싱글턴을 만들었다면 Serializable을 구현하는 것만으로는 직렬화하기 부족합니다.
역직렬화를 했을 때 새 인스턴스가 만들어져 싱글톤이 깨지기 때문입니다.
그렇기 때문에 모든 인스턴스 필드를 transient로 선언하고 readResolve() 메서드를 제공해야 합니다.
// 싱글턴임을 보장해주는 readResolve 메서드
private Object readResolve() {
// 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
return INSTANCE;
}
싱글턴의 가장 바람직한 세 번째 생성 방법도 알아볼까요?
열거 타입으로도 싱글턴을 만들 수 있다!
public enum Elvis {
INSTANCE;
...
}
열거 타입을 만들면 위의 두 생성 방식보다 더 간단하고, 추가 노력 없이 직렬화를 할 수 있는 싱글턴을 만들 수 있습니다.
대부분의 상황에서는 이렇게 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법입니다.