5장. 싱글턴 패턴
디자인 패턴 중 생성 패턴
- 팩토리 메소드
- 추상 팩토리
- 빌더
- 싱글턴
- 프로토타입
싱글턴 패턴
클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근(액세스) 지점을 제공하는 생성 디자인 패턴
생성자를 private 으로 지정하여 인스턴스 생성을 불가능하게 한 후, getInstance() 라는 정적 메서드를 구현하여 어디서든 호출 가능하게 하여 인스턴스를 관리한다.
용도: 연결 풀, 스레드 풀과 같은 자원 풀 관리, 캐시, 대화상자, 사용자 설정, 레지스트리 설정 처리, 로그 기록용 객체, 디바이스 드라이버 등
또한 추상팩토리 패턴을 싱글턴으로 구현 가능하다.
싱글턴 대신 전역 변수를 사용한다면
- 애플리케이션이 끝날 때까지 그 객체를 한 번도 안 쓸 경우 괜히 자원만 잡아먹음
- 싱글턴의 게으른 생성(Lazy Instantiation)을 사용할 수 없음
싱글턴 패턴의 문제점
1. 단일 책임 원칙 위배
- 자신의 인스턴스를 관리하는 일 외에도 원래 그 인스턴스를 사용하고자 하는 목적에 부합하는 작업을 책임져야 함
2. 클래스 로더가 여러 개라면 같은 클래스를 여러 번 로딩할 수도 있음
→ 클래스 로더를 직접 지정해주면 해결 가능
3. 리플렉션, 직렬화, 역직렬화 시 문제가 될 수 있음
→ enum 으로 싱글턴을 생성해서 해결 가능
4. 느슨한 결합 원칙 위배
- 싱글턴 객체에 의존하는 객체는 전부 하나의 객체에 단단하게 결합됨
- 싱글턴을 바꾸면 연결된 모든 객체를 바꿔야 할 가능성
5. 멀티 스레드 환경에서 여러 스레드가 싱글턴 객체를 여러 번 생성하지 않도록 특별한 처리가 필요
6. 서브 클래스 생성 불가능
- private 생성자 → 확장 불가능
- 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존하기 때문에, 싱글턴 클라이언트 코드의 단위 테스트를 하기 어려움
싱글턴은 애초에 특수한 상황에서 제한된 용도로 사용하려고 만들어졌기 때문에, 싱글턴을 많이 사용했다면 전반적인 디자인을 다시 한 번 생각해봐야 함
싱글턴의 단점
- 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존하기 때문에, 싱글턴 클라이언트 코드의 단위 테스트를 하기 어려움
- 대부분 언어에서 정적 메서드를 오버라이딩하는 것이 불가능
- 싱글턴의 한계를 극복할 수 있는 창의적인 방법을 생각해야함
- 아니면 그냥 테스트를 작성하지 말거나 싱글턴 패턴을 사용하지 않으면 됨
역직렬화, 리플렉션 시 문제 발생
역직렬화
- 역직렬화 자체가 보이지 않은 생성자로서 역할을 수행하기 때문에 인스턴스를 또 다시 만들어, 직렬화에 사용한 인스턴스와는 전혀 다른 인스턴스를 생성
- 해결방법: readResolve 메서드 사용
- readResolve 메서드를 정의하게 되면, 역직렬화 과정에서 readObject를 통해 만들어진 인스턴스 대신 readResolve에서 반환되는 인스턴스를 내가 원하는 것으로 바꿀 수 있음
- 기존에 역직렬화를 통해 생성된 객체는 자연스럽게 가비지 컬렉션 대상이 됨
- 싱글턴 객체 내 필드 변수들이 있을 경우, 실제로 직렬화 시에는 readResolve 로 바꿔치기 하기 때문에 실 데이터가 필요 없어 모든 인스턴스 필드를 transient로 선언해주는 것이 좋다.
- 역직렬화 과정 중간에 역직렬화된 인스턴스의 참조를 훔쳐오는 공격을 행할 경우 다른 객체로 바뀔 위험이 있기 때문
class Singleton implements Serializable {
// 싱글톤 객체의 필드들을 transient 설정하여 직렬화 제외
transient String str = "";
transient ArrayList lists = new ArrayList();
transient Integer[] integers;
private Singleton() {}
private static class SettingsHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SettingsHolder.INSTANCE;
}
// 역직렬화한 객체는 무시하고 클래스 초기화 때 만들어진 인스턴스를 반환
private Object readResolve() {
return SettingsHolder.INSTANCE;
}
}
리플렉션
- 클래스 객체를 통해 해당 객체의 생성자를 받아와 newInstance() 메서드를 실행하면 인스턴스를 생성할 수 있게 되는데, 여기서 생성된 인스턴스는 Holder가 가지고 있는 인스턴스와는 전혀 다른 새로운 인스턴스이기 때문
public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
/* Reflection API */
// 1. Singleton의 Class에서 생성자를 가져온다
Constructor<Singleton> consructor = Singleton.class.getDeclaredConstructor();
// 2. 생성자가 private 이기 때문에 외부에서 access 할 수 있도록 true 설정
consructor.setAccessible(true);
// 3. 가져온 생성자를 이용해 인스턴스화 한다
Singleton singleton1 = consructor.newInstance();
Singleton singleton2 = consructor.newInstance();
System.out.println("singleton1 == singleton2 : " + (singleton1 == singleton2));
}
// 실행 결과
// singleton1 == singleton2 : false
직렬화 & 역직렬화는 readResolve() 를 구현함으로써 대응할 수 있었지만, 리플렉션은 대응 불가
Enum 으로 해결 가능
- 애초에 멤버를 만들때 private로 만들고 한번만 초기화 하기 때문에 Thread-Safe 하며, enum 내에서 상수 뿐만 아니라 변수나 메서드를 선언해 사용이 가능하기 때문에, 이를 이용해 독립된 싱글톤 클래스 처럼 응용이 가능
- 열거형은 리플렉션을 통해 newInstance() 를 실행하지 못하도록 막아 놓았기 때문에 애초에 리플렉션 동작이 불가능
- Enum은 기본적으로 serializable 인터페이스를 구현하고 있기 때문에 직렬화도 역시 가능
enum Singleton {
INSTANCE; // 싱글톤 인스턴스
// 이넘은 필드와 메서드도 가질 수 있다
private int value = 3;
public int getValue() {
return value;
}
}
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println("singleton1 == singleton2 : " + (singleton1 == singleton2));
System.out.println(singleton1.getValue());
}
// 실행 결과
singleton1 == singleton2 : true
3
public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
/* Reflection API */
// 1. Singleton Enum의 생성자는 숨겨져 있기 때문에 getDeclaredConstructors로 배열로 가져온다.
Constructor<?>[] consructors = Singleton.class.getDeclaredConstructors();
// 2. 생성자 배열을 순회하여 인스턴스를 생성한다
for(Constructor<?> constructor : consructors){
constructor.setAccessible(true); // 생성자가 private 이기 때문에 외부에서 access 할 수 있도록 true 설정
Singleton singleton = (Singleton) constructor.newInstance("INSTANCE");
}
}
Enum 싱글턴의 단점
싱글턴으로 구성할 클래스가 특정 클래스의 상속이 필요한 구성일 경우, enum은 같은 enum 이 외의 클래스 상속은 불가능하기 때문에 어쩔수 없이 일반적인 클래스로 구성해야 한다.