[Java] Enum과 싱글톤(Singleton)
싱글톤이란?
· 하나의 JVM 당 하나의 인스턴스만 존재하는 클래스
· 동일한 싱글턴 인스턴스가 여러 개의 스레드에서 재사용됨
싱글톤을 만드는 기존의 방법
1. public static final field
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton(){};
}
2. public static factory method
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){};
public static Singleton getInstance(){
return INSTANCE;
}
}
3. lazy itialization
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton(){};
public static Singleton getInstance(){
if(INSTANCE == null){
synchronized (Singleton.class){
if(INSTANCE == null)
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
기존 방법의 장단점
· 위 방법 모두 코드 블럭에서 하는 일이 없어도 private 생성자를 만들어야 한다 (기본 생성자가 생성되는 걸 피하기 위해)
· 2, 3번 방법은 api를 변경하지 않고 클래스를 비싱글톤으로 만들수 있다는 (사소한) 장점이 있다.
public static Singleton getInstance() {
return new Singleton ();
}
· Static 필드는 클래스 로딩 시간에 초기화되므로 1, 2번 방법은 런타임에 해당 클래스를 사용하지 않아도 싱글톤 인스턴스가 생성 (자원 낭비)
· 3번 방법은 이러한 자원 낭비를 막고, 싱글톤 객체에 처음 액세스할 때 인스턴스가 생성된다. 또한 synchronized 예약어를 통해 멀티 스레드 환경에서 객체가 두 개 이상 생성되지 않도록 막는다.
추가적인 문제1 - 직렬화와 역직렬화 (Serialization & Deserialization)
· 클래스를 역직렬화할 때 새로운 인스턴스가 생성되어 싱글턴 속성을 위반한다.
public class Singleton implements Serializable {
public static Singleton INSTANCE = new Singleton();
private int value;
private Singleton(){};
public void setValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
public class SerializeDemo {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.setValue(1);
// Serialize
try {
FileOutputStream fileOut = new FileOutputStream("out.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(singleton);
out.close();
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
singleton.setValue(2);
// Deserialize
Singleton singleton2 = null;
try {
FileInputStream fileIn = new FileInputStream("out.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
singleton2 = (Singleton) in.readObject();
in.close();
fileIn.close();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c) {
System.out.println("singletons.SingletonEnum class not found");
c.printStackTrace();
}
if (singleton == singleton2) {
System.out.println("Two objects are same");
} else {
System.out.println("Two objects are not same");
}
System.out.println(singleton.getValue());
System.out.println(singleton2.getValue());
}
}
해결방법
· 싱글톤 클래스에 readResolve 메서드를 구현한다.
Serializable 및 Externalizable 클래스에서, readResolve 메서드는 클래스가 스트림에서 읽은 객체가 호출자에게 반환되전에 교체될 수 있도록 한다. readResolve 메서드를 구현함으로써 클래스는 역직렬화되는 인스턴스를 직접 제어할 수 있다.
readResolve 메서드는 ObjectInputStream이 스트림에서 객체를 읽고 이를 호출자에게 반환할 준비를 할 때 호출된다.
public class Singleton implements Serializable {
public static Singleton INSTANCE = new Singleton();
private int value;
private Singleton(){};
protected Object readResolve() {
return INSTANCE;
}
public void setValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
추가적인 문제2 - 리플렉션 (Reflectiom)
· 리플렉션을 이용하면 런타임에 private 생성자에 접근하여 새로운 인스턴스를 생성할 수 있다.
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.INSTANCE;
Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
Singleton singleton2 = (Singleton) constructor.newInstance();
if (singleton == singleton2) {
System.out.println("Two objects are same");
} else {
System.out.println("Two objects are not same");
}
singleton.setValue(1);
singleton2.setValue(2);
System.out.println(singleton.getValue());
System.out.println(singleton2.getValue());
}
}
Enum을 통해 싱글톤 만들기
· 위의 모든 문제는 enum 타입을 사용하여 싱글톤을 만들면 쉽게 해결 가능
public enum Singleton {
INSTANCE;
}
· enum 타입은 기본적으로 직렬화 가능하므로 Serializable 인터페이스를 구현할 필요가 없고, 리플렉션 문제도 발생하지 않는다.
· 인스턴스가 JVM 내에 하나만 존재한다는 것이 100% 보장 되므로, Java에서 싱글톤을 만드는 가장 좋은 방법으로 권장된다.
사용법
public enum SingletonEnum {
INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class EnumDemo {
public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.INSTANCE;
System.out.println(singleton.getValue());
singleton.setValue(2);
System.out.println(singleton.getValue());
}
}
실행 결과
주의사항!
열거형을 직렬화할 때 필드 변수는 소실된다. 즉, 위 코드에서 value 변수는 직렬화되지 않고, 소실된다.
출처
https://dzone.com/articles/java-singletons-using-enum