아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라. - ksw6169/effective-java GitHub Wiki
- 싱글톤은 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
- 클래스를 싱글톤으로 만들면 테스트가 어려워질 수 있다. (타입을 인터페이스로 정의하고 이를 구현한 싱글톤이 아니라면 싱글톤을 mock 객체로 대체할 수 없기 때문)
- ex. 무상태(stateless) 객체(인스턴스 필드를 가지지 않는 클래스의 객체), 설계상 유일해야 하는 시스템 컴포넌트
@Getter
public class Elvis {
public static final Elvis INSTANCE = new Elvis(30);
private int age;
private Elvis(int age) {
this.age = age;
}
}
이 방식은 리플렉션 API인 AccessableObject.setAccessible
을 사용해 private 생성자를 호출하여 두 번째 객체를 생성할 수 있으므로 싱글톤임을 보장하지 못할 수 있다.
@Test
public void test() throws Exception {
assertEquals(Elvis.INSTANCE.getAge(), 30);
Constructor constructor = Elvis.class.getDeclaredConstructor(int.class);
constructor.setAccessible(true);
Elvis newElvis = (Elvis) constructor.newInstance(20);
assertEquals(newElvis.getAge(), 20);
}
이럴 때는 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
@Getter
public class Elvis {
public static final Elvis INSTANCE = new Elvis(30);
private int age;
private Elvis(int age) {
if (INSTANCE != null)
throw new UnsupportedOperationException();
this.age = age;
}
}
[장점]
- 간결하다.
- 싱글톤 클래스임이 API에 명백히 드러난다.
정적 팩토리 방식의 싱글톤도 리플렉션을 통한 private 생성자 호출이 가능하다. 따라서 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
@Getter
public class Elvis {
private static final Elvis INSTANCE = new Elvis(30);
private int age;
private Elvis(int age) {
this.age = age;
}
public static Elvis getInstance() {
return INSTANCE;
}
}
[장점]
-
API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다.
-
getInstance()
를 호출하는 스레드별로 다른 인스턴스를 넘겨주게 변경할 수 있다.
-
-
정적 팩토리를 제네릭 싱글톤 팩토리로 만들 수 있다.
-
정적 팩토리 메소드 참조를 공급자로 사용할 수 있다.
-
Elvis::getInstance
를Supplier<Elvis>
로 사용할 수 있다.
-
public enum Elvis {
INSTANCE;
}
[장점]
- 간결하다.
- 추가 노력 없이 직렬화가 가능하다.
- 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
- 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글톤을 만드는 가장 좋은 방법이다.
싱글톤 클래스를 직렬화하려면 Serializable
만 구현하면 안된다. 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스를 생성하기 때문이다. (이로 인해 해당 객체가 싱글톤임을 보장하지 못한다.)
// 직렬화 대상 클래스
public class Elvis implements Serializable {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public static Elvis getInstance() {
return INSTANCE;
}
}
// 직렬화 시 사용할 MySerializer 유틸 클래스
public class MySerializer {
public static byte[] serialize(Object instance) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(instance);
} catch (IOException e) {
e.printStackTrace();
}
return bos.toByteArray();
}
public static Object deserialize(byte[] serializedBytes) {
ByteArrayInputStream bis = new ByteArrayInputStream(serializedBytes);
try (ObjectInputStream ois = new ObjectInputStream(bis)) {
return ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
@Test
public void test() {
Elvis singleton1 = Elvis.getInstance();
byte[] serializedBytes = MySerializer.serialize(singleton1);
// 역직렬화 할때마다 새로운 인스턴스를 생성한다.
Elvis singleton2 = (Elvis) MySerializer.deserialize(serializedBytes);
// 따라서 singleton1 != singleton2 이므로 실패한다.
assertEquals(singleton1, singleton2);
}
이 때는 모든 인스턴스 필드를 trasient
로 선언하고 readResolve()
를 제공하면 된다. (readResolve()
에서 항상 같은 싱글톤을 반환하도록 메소드를 추가해준다.)
public class Elvis implements Serializable {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public static Elvis getInstance() {
return INSTANCE;
}
// 싱글톤임을 보장해주는 readResolve 메소드
private Object readResolve() {
// 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
return INSTANCE;
}
}
- Effective Java 3/E (Joshua Bloch)
- 자바 직렬화: readResolve와 writeReplace
- 자바 직렬화: writeObject와 readObject