싱글턴(Singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
싱글턴에 대해서는 필자가 정리해 둔 글을 참고하자.
클래스를 싱글톤으로 만들면, 이에 대한 테스트코드 작성이 어려워질 수 있다.
타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(Mock) 구현으로 대체할 수 없기 때문이다.
싱글턴을 구현하는 방법은 보통 두 가지 방법으로 분류할 수 있다.
두 방식 모두 생성자는 private로 감춰두고, 인스턴스에 접근할 수 있는 유일한 수단으로 public static 멤버를 하나 마련해둔다.
1. public static 멤버가 final 필드인 방식
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public void leaveTheBuilding(){}
}
private 생성자는 public static final 필드인 Elivis.INSTANCE를 초기화할 때 딱 한번만 호출된다.
public이나 protected 생성자가 없으므로 Evlis 타입의 인스턴스는 전체 애플리케이션에서 단 하나뿐임이 보장된다.
그러나 이는 리플렉션 혹은 역직렬화를 통해 싱글톤이 깨질 수 있다.
어떻게 예방할까?
리플렉션의 경우 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
(필자가 아직 리플렉션을 제대로 공부하지 않아서 어렵기 때문에 이에 대해서는 나중에 추가하도록 하겠다.)
역직렬화의 경우 readResolve() 메서드를 구현하여 새로운 인스턴스가 반환되는 것을 예방해주면 된다.
다음은 역직렬화를 통한 싱글톤이 깨지는 예시이다.
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public void leaveTheBuilding(){}
}
class SerializingTest{
public static void main(String[] args) {
Elvis serializing = serializing();
Elvis deserializing = deserializing();
System.out.println();
System.out.println();
System.out.println("Elvis.INSTANCE : " + Elvis.INSTANCE);
System.out.println("serializing : " + serializing);
System.out.println("deserializing : " + deserializing);
}
public static Elvis serializing() {
try {
FileOutputStream fos = new FileOutputStream("user.ser");
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream out = new ObjectOutputStream(bos);
Elvis elvis = Elvis.INSTANCE;
out.writeObject(Elvis.INSTANCE);
out.close();
System.out.println("직렬화 완료");
return elvis;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static Elvis deserializing(){
try { FileInputStream fis = new FileInputStream("user.ser");
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream in = new ObjectInputStream(bis);
Elvis deserializingElvis = (Elvis) in.readObject();
System.out.print("deserializingElvis == Elvis.INSTANCE -> ");
System.out.println(deserializingElvis == Elvis.INSTANCE);
System.out.println("deserializingElvis :" + deserializingElvis);
System.out.println("Elvis.INSTANCE :" +Elvis.INSTANCE);
in.close();
return deserializingElvis;
} catch (Exception e) { // TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
}
다음은 역직렬화시 싱글톤이 깨지지 않도록 예방하는 예시이다
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public void leaveTheBuilding(){}
@Serial
private Object readResolve(){
System.out.println("역직렬화시 호출");
return INSTANCE;
}
}
class SerializingTest{
public static void main(String[] args) {
Elvis serializing = serializing();
Elvis deserializing = deserializing();
System.out.println();
System.out.println();
System.out.println("Elvis.INSTANCE : " + Elvis.INSTANCE);
System.out.println("serializing : " + serializing);
System.out.println("deserializing : " + deserializing);
}
public static Elvis serializing() {
try {
FileOutputStream fos = new FileOutputStream("user.ser");
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream out = new ObjectOutputStream(bos);
Elvis elvis = Elvis.INSTANCE;
out.writeObject(Elvis.INSTANCE);
out.close();
System.out.println("직렬화 완료");
return elvis;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static Elvis deserializing(){
try { FileInputStream fis = new FileInputStream("user.ser");
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream in = new ObjectInputStream(bis);
Elvis deserializingElvis = (Elvis) in.readObject();
System.out.print("deserializingElvis == Elvis.INSTANCE -> ");
System.out.println(deserializingElvis == Elvis.INSTANCE);
System.out.println("deserializingElvis :" + deserializingElvis);
System.out.println("Elvis.INSTANCE :" +Elvis.INSTANCE);
in.close();
return deserializingElvis;
} catch (Exception e) { // TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
}
2. public static 멤버가 정적 팩터리 메서드인 방식
public class Elvis {
private static final transient Elvis INSTANCE = new Elvis();
private Elvis(){}
public static Elvis getInstance(){
return INSTANCE;
}
public void leaveTheBuilding(){}
}
위의 경우에도 리플렉션과 역직렬화의 경우 똑같이 싱글톤이 깨질 수 있다.
각 방식의 장점
public static 멤버가 final 필드인 방식
- 해당 클래스가 싱글턴임이 API에 명백히 드러난다
- 매우 간결하다.
public static 멤버가 정적 팩터리 메서드인 방식
- API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다. 유일한 인스턴스를 반환하던 팩터리 메서드가 (예를 들면) 호출하는 스레드별로 다를 인스턴스를 넘겨주게 할 수 있다.
- 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. (제네릭 싱글턴 팩터리에 대해서는 아이템 30에서 설명한다)
- 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다.
두 방식 모두 싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현하는 것만으로는 부족하다.
모든 인스턴스 필드를 transient로 선언하고(정적 필드는 상관없다. 인스턴스 필드만 하면 된다.), readResolve 메서드를 제공해야 한다.(아이템 89)
Enum으로 만드는 싱글턴
싱글턴을 만드는 세 번째 방법으로 원소가 하나인 열거 타입(Enum)을 선언하는 것이다.
public enum Elvis {
INSTANCE;
public void leaveTheBuilding(){}
}
public 필드 방식과 유사하나, 더 간결하고 추가 노력 없이 직렬화할 수 있으며, 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 싱글톤이 깨지지 않도록 방어해준다.
조금 부자연스러워 보일 수는 있으나, 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
단 만들려는 싱글턴이 Enum외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.
(그러나 열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.)
'☕️ Java > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] 아이템 6 - 불필요한 객체 생성을 피하라 (0) | 2022.02.03 |
---|---|
[Effective Java] 아이템 5 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.02.02 |
[Effective Java] 아이템 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.02.02 |
[Effective Java] 아이템 2 - 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2022.01.23 |
[Effective Java] 아이템 1 - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.01.22 |