-
SingletonCS/Design Pattern 2022. 8. 6. 18:15
해당 글은 Singleton 객체가 무엇인지, 필요성, 생성 방법, Singleton을 깨트리는 요소들과 이를 대응하는 방법에 대해 기술합니다.
Singleton?
인스턴스를 오직 하나만 만드는 클래스 (public, global 접근 가능한)
필요성
어떤 서비스에서 유일하게 존재하는 기능인 경우
ex) 사용자마다 필요한 설정 기능
사용자 개인이 해둔 설정은 수정하지 않는 이상 변함이 없어야 한다.
- 만약 설정 화면을 들어 갈 때마다, 인스턴스를 새로 생성한다면? -> 사용자는 매번 설정을 다시 해야함.public class Singleton { }
Singleton class가 존재할 때, 외부 클래스에서 호출해 사용하는 경우 아래와 같다.
public class Main { public static void main(String[] args) { Singleton singleton = new Singleton(); } }
하지만, new 생성자를 통해 호출하는 경우 단일 인스턴스를 보장할 수 없다.
public class Main { public static void main(String[] args) { Singleton singleton1 = new Singleton(); Singleton singleton2 = new Singleton(); System.out.println(singleton1 != singleton2); // true } }
방법 1.
- private을 통해 생성자 접근을 막는다.
- 인스턴스는 메모리에 올라갈 때 단 한 번 생성될 수 있도록 static method를 선언해준다.
하지만, getInstance 메서드가 호출 될 때마다. 생성자를 매번 호출하니 단일 인스턴스가 보장되지 않는다.
public class Singleton { private Singleton() {} public static Singleton getInstance() { return new Singleton(); } }
public class Main { public static void main(String[] args) { Singleton singleton1 = Singleton.getInstance(); Singleton singleton2 = Singleton.getInstance(); System.out.println(singleton1 != singleton2); // true } }
- 클래스 자체 내에서 인스턴스를 생성해 반환하는 방식으로 코드를 변경해 해결한다.
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
public class Main { public static void main(String[] args) { Singleton singleton1 = Singleton.getInstance(); Singleton singleton2 = Singleton.getInstance(); System.out.println(singleton1 != singleton2); // false } }
Multi-thread 환경이라면 해당 코드는 사용할 수 없다.
ex) thread가 2개인 경우 (동기화 x)
thread1에서 instance를 생성하고 있을 때, thread2에서도 같은 코드에 접근하면 thread2는 thread1의 동작을 알 수 없기 때문에 instance 값을 null로 인식해 인스턴스를 또 생성한다.즉, Mutli-thread 환경에서도 단일 instance를 보장할 적절한 동기화가 필요
방법 2.
- 위의 코드에서 말 그대로 동기화 키워드를 선언한다.
- 성능적 아쉬움이 있음
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
synchronized 키워드는 해당 블럭을 critical section화 해서 한 번에 하나의 스레드만 접근 가능하도록 한다.
하나의 공유 key를 생성해 section에 접근하려는 스레드에 key를 할당하고 해제하는 방식으로 성능 부하가 존재한다.
(key가 있는지 확인하고, 없으면 할당하고, 작업 진행 후 키 해제 등 여러 작업이 계속해서 이루어짐)
방법 3.
- Eager initialization
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
thread-safe
INSTANCE는 미리 static하게 class 로딩 시점에 생성되고 final하게 고정된다.
(다른 코드 변경은 없음)
미리 만드는 것이 단점
로딩시에 미리 만들어 놨으나 쓰질 않으면 낭비
Lazy하게 getInstance를 통해 호출될 때, 즉 사용이 될 때 만드는 것이 best.
방법 4.
- Double checked locking
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
thread1이 sychronized로 접근 후 thread2가 if(instance == null)을 거쳐도, key가 없으니 synchronized를 진입할 수 없다.
따라서, thread1이 intsance를 생성 한 후 key가 반납되면 이후 코드에 접근 가능하므로 thread-safe를 보장할 수 있다.
또한, 필요한 시점에 생성할 수 있다는 장점도 있다.
방법 2와는 확연히 차이가 있다.
getInstance 접근시 매번 key 동기화에 대한 작업이 걸리지만, double checked locking은 그 횟수가 현저히 줄어듦.jdk 1.5 이상에서만 사용 가능하다. 코드의
가독성이 좋지 않다.
volatile에 대해 잘 알지 못하는데 막 사용해도 될까? (이론적 배경 지식 부족)
방법 5.
- static inner class
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
Singleton 내부에서만 사용될 수 있도록 private으로 접근을 막아준다.
이렇게 하면 JVM에서 중첩 클래스를 lazy loading 하기 때문에 thread-safe하다.
그러나 이 방법도 편법을 통해 Singleton의 고유성을 깨트릴 수 있다.
편법 1.
- reflection
import java.lang.reflect.InvocationTargetException; public class Main { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException { Singleton singleton1 = Singleton.getInstance(); // reflection Constructor<Singleton> constructor = Settings.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton singleton2 = constructor.newInstance(); System.out.println(singleton1 != singleton2); // true } }
왜 이렇게 씀? 누가?? 그럴리가 있냐? 라고 하면 할 말 없지만, 어떤 위험성도 존재하지 않도록 하는것이 개발자의 자세 아닐까.
편법 2.
- 직렬화, 역직렬화
- Serializable interface를 구현하는 방식으로 객체를 파일로 저장 후 로딩이 가능함
- 문제는 역직렬화 하는 경우 반드시 생성자를 호출해 다시 한 번 인스턴스를 생성하기 때문에 Singleton이 깨진다.
public class Singleton implements Serializable { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
import java.io.*; public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { Singleton singleton1 = Singleton.getInstance(); Singleton singleton2 = null; try(ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) { out.writeObject(singleton1); } try(ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) { singleton2 = (Singleton) in.readObject(singleton1); } System.out.println(singleton1 != singleton2); // true } }
역직렬화 대응 방안
역직렬화시 반드시 사용하는 method(readResolve)를 Singleton 객체에서 getInstance를 호출하도록 처리
즉, 메서드 재정의를 통해 역직렬화의 문제점을 해결한다.
public class Singleton implements Serializable { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } protected Object readResolve() { return getInstance(); } }
그러나 reflection은 어떻게 대응이 안된다...
방법 6.
- enum (reflection 대응 방법)
public enum Singleton { INSTANCE; }
import java.lang.reflect.InvocationTargetException; public class Main { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException { Singleton singleton1 = Singleton.getInstance(); // reflection Singleton singleton2 = null; Constructor<?>[] constructors = Settings.class.getDeclaredConstructors(); for(Constructor<?> constructor: constructors) { constructor.setAccessible(true); singleton2 = (Singleton) constructor.newInstance("INSTANCE"); // IllegalArgumentException, reflection에서 내부적으로 enum을 막는다. } System.out.println(singleton1 != singleton2); // true } }
enum은 클래스 로딩시 미리 생성된다는 것이 아쉬운 점이다.
하지만, 직렬화 역직렬화에도 안전하다. (enum은 enum 추상 클래스를 상속 -> 해당 클래스는 Serializable을 구현하고 있음)
참고 자료
GoF 디자인 패턴 - 에릭 감마, 존 블리사이드스, 랄프 존슨, 리하르트 헬름
반응형'CS > Design Pattern' 카테고리의 다른 글
Factory Method pattern & Abstract Factory Pattern (1) 2022.08.27