Effective Java 3/E
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋겠다. 그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자!
아이템3: 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글턴?
인스턴스를 오직 하나만 생성할 수 있는 클래스
싱글턴을 만드는 방식
1. public static final 필드 방식
2. static factory method 방식
3. 원소가 1개인 ENUM 타입
1. public static final 필드 방식의 싱글턴
생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단을 public static final 필드로 제한한다.
public static final 필드 방식의 가장 큰 장점은 해당 클래스가 싱글턴임이 API에 명확히 드러난다는 점이다.
public class Singleton {
//초기화 할때 한번만 설정됨으로 하나의 인스턴스만을 보장 할 수 있음
public static final Singleton INSTANCE = new Singleton();
// 생성자를 private 으로 선언하여 외부 클라이언트에서 접근 불가능
private Singleton() {
// 리플렉션 방어 코드
if(INSTANCE != null) {
throw new Exception("생성 불가");
}
}
}
2. 정적 팩터리 메서드를 public static 맴버로 제공
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { ... }
public static Singleton getInstance() {
return INSTANCE;
}
}
이 방식도 자바 리플렉션으로 접근이 가능하니 public staitc fianl 방식과 같이 예외처리를 해줘야한다.
API 를 변경하지 않고도 싱글턴 사용 여부를 변경할 수 있다.
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance(){
// 해당 부분만 변경하여 싱글톤 사용여부를 변경가능
return new Singleton();
}
}
정적 팩터리의 메서드를 Supplier<> 에 대한 메소드 레퍼런스로 사용할 수 있다.
Supplier<Singleton> supplier = Singleton::getInstance;
Singleton instance = supplier.get();
위의 두 방식 중 하나로 만든 싱글턴 클래스를 직렬화하려면 단순히 `Serializable`을 구현한다고 선언하는 것만으로 부족하다. 모든 인스턴스 필드를 일시적(transient)이라 선언하고 `readResolve` 메서드를 제공해야 한다. 이렇게 하지 않으면 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어진다. 역직렬화 시 호출하는 숨겨진 기본 `readResolve` 함수에서는 return new Object를 해서 새로운 인스턴스가 생성된다.
import java.io.Serializable;
public final class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
// readResolve 메서드를 정의한다.
private Object readResolve() {
// 싱글턴을 보장하기 위함
return INSTANCE;
}
}
3. 원소가 1개인 ENUM 타입
첫 번째 public static final 필드 방식과 비슷하지만 더 간결하고, 추가 노력 없이 직렬화 가능하며, 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제 2의 인스턴스가 생기는 일을 완벽히 막아준다. 이 책에선 원소가 하나뿐인 열거타입이 싱글턴을 만드는 가장 좋은 방법이라고 소개하고 있다. 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방식은 사용할 수 없다.
public enum Singleton {
INSTANCE;
...
}
+ 다양한 싱글톤 구현 방식
1. Eager Initialization (이른 초기화, Thread-safe)
- 위에서 소개한 public static final 필드 방식이다.
- 애플리케이션에서 해당 인스턴스를 사용하지 않더라도 인스턴스를 생성하기 때문에 낭비가 발생할 수 있다.
- 싱글톤 클래스가 다소 적은 리소스를 다룰 때 사용하는 것이 좋다
2. static block initialization (정적블럭 초기화, Thread-safe)
- Eager Initalization 방식에 Exception Handling을 추가한 방식이다.
public class Singleton {
private static Singleton instance;
private Singleton(){}
static{
try{
instance = new Singleton();
}catch(Exception e){
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
public static Singleton getInstance(){
return instance;
}
}
3. Lazy Initialization (게으른 초기화)
- 앞선 두 방식과 다르게 나중에 초기화 하는 방식이다.
- Eager 방식의 사용되지 않는 인스턴스를 생성한다는 문제에 대한 해결책이 될 수 있다. 하지만 멀티 쓰레드 환경에서 인스턴스가 생성되지 않은 시점에 여러 쓰레드가 동시에 getInstance() 를 호출하게 된다면 동기화 문제가 발생할 우려가 있다.
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
4. Lazy Initialization with synchronized ( 동기화 블럭을 사용한 게으른 초기화, Thread-safe )
- 1,2 Eager 방식, 3 Lazy Initialization 방식의 단점을 모두 해결할 수 있다.
- synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은 애플리케이션에 대해서는 성능이 떨어지게 된다.
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
5. Lazy Initialization, Double Checking Locking ( DCL, Thread-safe )
- 4번의 문제를 해결하기 위해 인스턴스가 생성되어 있지 않은 시점에만 synchronized가 실행되게끔 구현하는 방식이다.
public class Singleton {
private volatile static Singleton instance;
private Sigleton() {}
// Lazy Initialization. DCL
public Singleton getInstance() {
// 첫 부분에서 객체를 만들어야 하는 지를 먼저 확인하고 객체를 만들어야 할때만 락을 얻어야 하도록 만들 수 있다.
if(instance == null) {
synchronized(Singleton.class) {
// 연산의 원자성을 유지하기 위해서 동기화 블록에 들어가자마자 같은 확인 작업을 수행할 수 있다.
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
이 패턴을 사용할 때는 반드시 필드를 volatile로 해야 한다. volatile 키워드를 이용해서 캐시 불일치 이슈를 방지 할 수 있다. 실제로 자바 메모리 모델은 부분적으로 초기화 된 객체의 발행을 허용하기 때문에 파악하기 어려운 버그를 만들어 낼 수 있다.
6. Lazy initialization, LazyHolder ( 게으른 홀더, Thread-safe )
- inner static helper class를 사용하는 방식인데, volatile 이나 synchronized 키워드 없이도 동시성 문제를 해결하기 때문에 성능이 뛰어나며 현재 가장 널리쓰이는 싱글톤 구현 방식이다.
public class Singleton {
private Singleton() {}
private static class InnerInstanceClazz {
// 클래스 로딩 시점에서 생성
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InnerInstanceClazz.INSTANCE;
}
}
- private inner static class를 두어 싱글톤 인스턴스를 갖게 한다.
- InnerInstanceClazz클래스는 Singleton 클래스가 Load 될 때에도 로드되지 않다가 getInstance가 호출 되었을 때 JVM 메모리에 로드 되고, 인스턴스를 생성하게 된다.
7. Enum Singleton ( Thread-safe )
- 위에 살펴본 원소가 1개인 Enum 방식과 동일하다.