Item 3 - private 생성자나 열거타입으로 싱글턴임을 보증하라
싱글턴(Singleton)이란?
- 인스턴스를 오직 하나만 생성할 수 있는 클래스
- 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트, DBCP(DataBase Connection Pool) 등
- 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워진다.
- 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.
싱글턴을 만드는 방식
(1) private 생성자 + public static final 필드
1
2
3
4
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
}
- private 생성자는 public static final 필드인
Elvis.INSTANCE
가 초기화될 때 한번만 호출된다. - public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나 뿐임을 보장한다.
장점 👍
- 해당 클래스가 싱글턴임이 API에 명백히 드러난다.
- 간결하다.
단, 리플렉션 API를 사용하면 private 생성자를 호출할 수 있어 새로운 인스턴스를 생성할 수 있다.
이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체를 생성하려 할 때 예외를 던지면 된다.
1
2
3
4
5
6
7
8
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
if (INSANCE != null) {
throw new RunTimeException("생성자 호출 불가!");
}
}
}
(2) private 생성자 + 정적 팩터리 메서드
1
2
3
4
5
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
}
Elvis.getInstance
는 항상 같은 객체 참조를 반환하므로 제 2의 인스턴스를 만들지 않는다.- 이 방식도 리플렉션 API에 의해 예외적인 상황이 발생할 수는 있다.
장점 👍
- 정적 팩터리 메서드만 수정하면 언제든 싱글턴이 아니게 변경할 수 있다.
- 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. (타입에 대한 유연한 대처)
- 정적 팩터리의 메서드 참조를 공급자(Supplier)로 만들 수 있다.
- ex. Elvis::getInstance → Supplier
- ex. Elvis::getInstance → Supplier
두 방식의 유의점 🤔
1
2
3
4
5
// 싱글턴임을 보장해주는 readResolve 메서드
private Object readResolve() {
// '진짜' Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
return INSTANCE;
}
- (1)번과 (2)번 방식으로 만들어진 싱글턴 클래스를 직렬화하려면
Serializable
을 구현한다고 선언하는 것만으로는 부족하다. - 모든 인스턴스 필드에
transient
를 선언하고readResolve
메서드를 제공해야만, 역직렬화시에 새로운 인스턴스가 만들어지는 것을 방지할 수 있다.
(3) 열거 타입 방식의 싱글턴
1
2
3
public enum Elvis {
INSTANCE;
}
- 가장 간결한 방법이며 직렬화와 리플렉션에도 안전하다.
- 대부분의 상황에선 원소가 하나뿐인 열거타입이 싱글턴을 만드는 가장 좋은 방법이다. 👍
참고 🕶️
함수형 인터페이스
https://blogs.oracle.com/javamagazine/post/understanding-java-method-invocation-with-invokedynamic
https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html
객체 직렬화 스펙
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serialTOC.html
This post is licensed under CC BY 4.0 by the author.