럿고의 개발 노트
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라. 본문
Java Note/이펙티브 자바 3판(EFFECTIVE JAVA 3E)
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
KimSeYun 2020. 3. 16. 12:19이펙티브 자바 3판 - 2장. 객체 생성과 파괴
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
싱글턴(Singleton)
- 인스턴스를 오직 하나만 생성할 수 있는 클래스다.
- 전형적인 예로는 함수와 같은 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다.
- 그러나 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
- 왜냐하면 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.
- 싱글턴을 만드는 방법은 두가지 인데, 두가지 모두 생성자는
private
으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로public static
멤버를 하나 마련해둔다.
public static final 필드 방식의 싱글턴
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public void leaveTheBuilding(){
System.out.println("Whoa baby, I'm outta here!");
}
}
public class ElvisTest {
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();;
}
}
INSTANCE
는 초기화할때 딱 한번만 호출된다.public
이나protected
생성자가 없으므로 인스턴스가 하나임을 보장할 수 있다.,- 그러나 예외가 하나 있는데,
AccessibleObject.setAccessible
을 사용하면private
생성자를 호출할 수 있다.
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ElvisDestroyer {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
Elvis elvis = Elvis.getInstance();
System.out.println(elvis.toString());
Constructor[] constructors = Elvis.class.getDeclaredConstructors();
for (Constructor constructor : constructors){
constructor.setAccessible(true);
System.out.println(constructor.newInstance().toString());
break;
}
}
}
- 위의 코드처럼
newInstance()
를 통해 계속해서 다른 인스턴스를 생성할 수 있다. 이 문제에 대한 해결책은Enum
을 이용하는 방법이다. - 이걸 방어하기 위해서는 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
if(INSTANCE != null){
throw new IllegalArgumentException("생성자를 호출 할 수 없습니다.");
}
}
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
}
- 장점
- 해당 클래스가 싱글턴임이 API에서 명백히 드러난다는 것이다.
pubic static
필드가final
이니 절대로 다른 객체를 참조할 수 없다. - 간결함
- 해당 클래스가 싱글턴임이 API에서 명백히 드러난다는 것이다.
정적 팩터리 방식의 싱글턴
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
}
public class ElvisTest {
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
getInstance()
는 아이템 1에서 정적 팩터리 메서드에 흔히 사용하는 명명 방식에 설명되어 있다. 매개변수가 없는 싱글턴 패턴을 따를 경우 항상 같은 객체를 반환하는 의미로 사용한다.- 역시 리플렉션을 통한 예외는
public static final fleid
와 똑같이 적용된다. - 장점
- API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다. 유일한 인스턴스를 반환하던
getInstance()
가 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다. - 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들수 있다.
- 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다. 예를 들어
Elvis::getInstance
를Supplier<Elvis>
로 사용할 수 있다.
- API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다. 유일한 인스턴스를 반환하던
- 위의 3가지 장점들이 필요하지 않다면
public static final field
방식이 더 좋다.
싱글턴 클래스 직렬화
- 단순히
Serializable
을 구현한다고 선언하는 것만으로는 부족하다.
cf) 직렬화(Serializable)
- 자바 직렬화란 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트 형태로 데이터 변환하는 기술과 바이트로 변환된 데이터를 다시 객체로 변환하는 기술(역직렬화)을 아울러서 이야기합니다.
- 자바에서는 java.io.Serializable
인터페이스를 상속받은 객체는 직렬화 할 수 있는 기본 조건입니다.
import java.io.Serializable;
public class Elvis implements Serializable {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
}
import java.io.*;
public class ElvisSerialize {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Elvis elvis = Elvis.getInstance();
System.out.println(elvis.toString());
// 직렬화
ObjectOutput out = new ObjectOutputStream(new FileOutputStream("output.text"));
out.writeObject(elvis);
out.close();
// 역직렬화
ObjectInput in = new ObjectInputStream(new FileInputStream("output.text"));
Elvis elvis1 = (Elvis) in.readObject();
in.close();
System.out.println(elvis1.toString());
}
}
- 위와 같이 한다면 서로 다른
elvis
와elivs1
이 서로 다른 참조 객체를 가르킬 것 이다. readObject()
를 할때 새로운 인스턴스를 생성하기 때문이다.- 이를 해결해서 위해서
readResolve()
만 추가하면 된다.
import java.io.Serializable;
public class Elvis implements Serializable {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
public static Elvis getInstance() {
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
}
- 이렇게 한다면 역직렬화를 진행할때 계속 같은 참조 객체를 가르킬것 이다.
readResolve()
가 최초 생성된 인스턴스를readResolve()
를 통해 전달하면readObject
에서 생성된 인스턴스를 GC에 의해 해제됩니다.
열거 타입 방식의 싱글턴
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("금방 나갈께!");
}
}
public class ElvisTest {
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
- 더 간결하고, 추가 노력 없이 직렬화할 수 있고, 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
- 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
- 단, 만들려는 싱글턴이
Enum
외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.(열거 타입이 다른 인터페이스를 구현하도록 선언할 수 없다.)
'Java Note > 이펙티브 자바 3판(EFFECTIVE JAVA 3E)' 카테고리의 다른 글
아이템 6. 불필요한 객체 생성을 피하라. (0) | 2020.03.23 |
---|---|
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라. (0) | 2020.03.23 |
아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2020.03.17 |
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2020.03.15 |
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2020.03.14 |
Comments