Singleton 패턴은 한 Class에 한 Instance만 존재하도록 하는 OOP의 설계 방법이다.
아주 단순하게 Singleton을 아래와 같이 구현할 수 있다.
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
이런 식의 구현은 Multi-Thread 환경에서 문제를 겪을 수 있다.
이를 해결하는 방법은 3가지가 있다.
# Eager Initialization
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
}
첫번째 방법은 클래스를 생성하는 즉시 인스턴스를 만드는 방법이다.
이 방법에서 getInstance가 Thread-Safe하게 된다.
그런데 인스턴스를 만드는 비용이 비싸다면, 더 효율적인 방법을 생각해야 한다.
synchronized를 사용하는 방법은 다시 2가지로 나뉜다.
synchronized를 getInstance라는 메소드 앞에 붙이면 클래스 내 존재하는 Thread의 Sync를 맞춰준다.
첫번째 방법은 getInstance 메소드에 단순히 synchronized를 추가하는 것이고,
두번째 방법은 Double Checked Locking이란 방법이다.
# just add synchronized
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
첫번째 방법의 문제는 Singleton의 getInstance 메소드를 Multi-Thread로 활용할 수 없다는 것이다.
Multi Thread를 자주 호출하는 서비스에서 성능이 저하될 수 있다.
이를 해결하는 방법은 synchronized를 getInstance 내부에 배치하는 것이고, Double Checked Locking이라고 불린다.
# Double Check Locking
public class Singleton {
private static volatile Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
}
}
Double Checked Locking에선 Singleton Class의 Constructor에 volatile 이란 문법이 추가됐다.
이 문법은 Java의 메모리 관리에 대한 지식이 필요하다.
또 synchronized가 사용된 구조가 다른 방법에 비해 복잡하다.
public class Singleton {
private Singleton() { }
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
위의 3가지 방법으로 Thread-Safe한 Singleton를 설계했다.
Thread-Safe하면서 프로세스 내 하나의 인스턴스를 갖도록 설계했으나,
이를 사용하는 Client 코드에서 Singleton 패턴을 깨뜨릴 수 있는 2가지 Loophole이 있다.
첫번째는 리플렉션이다.
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class App {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException {
Singleton singletonA = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singletonB = constructor.newInstance();
System.out.println(singletonA == singletonB);
# false
}
}
두번째는 직렬화와 역직렬화이다.
Java에서는 실행중인 객체를 파일 형태로 저장하고 불러올 수 있다.
이 방법으로도 Singleton 설계를 깨트릴 수 있다.
public class App {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singletonA = Singleton.INSTANCE;
Singleton settingsB = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
out.writeObject(singletonA);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
singletonB = (Singleton) in.readObject();
}
System.out.println(singletonA == singletonB);
# false
}
}
2가지 Loophole을 해결할 수 있는 방법은 enum을 이용하여 클래스를 설계하는 것이다.
public enum Singleton {
INSTANCE;
}
이 방법의 단점은 class 생성시 인스턴스가 미리 생성된다는 것이다.