-
싱글톤 패턴 (Singleton Pattern)디자인 패턴 2022. 6. 4. 17:27
싱글톤 패턴이란?
- 인스턴스를 오직 하나만 제공하는 패턴
- 생성자가 여러차례 호출되더라도 계속해서 같은 객체를 리턴한 한다.
- 환경 세팅에 대한 정보 등 인스턴스가 여러개 일때 문제가 발생할 수 있는 경우가 있는데, 이 때 싱글톤 패턴을 사용하여 해결
기본 싱글톤 패턴 구현
package singleton; public class Settings { private static Settings instance; private Settings() {} public static Settings getInstance() { if (this.instance == null) { this.instance = new Settings(); } return this.instance; } }
1. 생성자를 private으로 만든 이유?
- new 를 사용해서 인스턴스를 생성하면 계속해서 인스턴스가 새로 만들어지기 때문에, 이를 막으려면 생성자를 pirvate 으로 선언해야 한다.
2. getInstance() 메소드를 static 으로 선언한 이유?
- 외부에서 생성자를 호출 못하도록 private 으로 막아 놓았기 때문에 외부에서는 인스턴스를 만들지 못한다. 따라서 해당 메서드를 호출하려면 전역으로 설정해두어야 한다.
3. getInstance()가 멀티 쓰레드 환경에서 안전하지 않은 이유?
- 여러 쓰레드가 인스턴스 생성 함수를 동시에 접근할 경우 각각의 다른 인스턴스가 생성된다.
멀티쓰레드 환경에서 안전하게 구현하는 방법
[방법 1]
- synchronized 키워드를 사용하여 여러 쓰레드가 동시에 접근하지 못하도록 막는다.
- 단점은 Synchronized 키워드 사용하면, 해당 메서드가 호출될때마다 동기화 처리 작업을 수행해야하는데 이는 성능에 불이익이 있을 수 있다.
- lock 을 사용해서 lock 을 가지고 있는 쓰레드만 접근할 수 있게끔 하는 메커니즘으로 되어져 있다.
package singleton; public class Settings { private static Settings instance; private Settings() {} public static synchronized Settings getInstance() { if (instance == null) instance = new Settings(); return instance; } }
[방법 2]
- 이른 초기화 (eagar initialization) 사용하기
- 해당 인스턴스를 클래스가 로딩되는 시점에 생성되도록 미리 만드는 방법이다.
- 단점은 미리 만든다는 자체가 단점이 될 수도 있다.
- 사용하지 않을수도 있는데, 메모리를 많이 차지하는 인스턴스 일 경우 미리 만들어 놓는것 자체가 부담일 수 있다.
public class Settings { private static final Settings INSTANCE = new Settings(); private Settings() {} public static Settings getInstance() { return INSTANCE; } }
[방법 3]
- double cheked locking 사용하기
- 인스턴스 존재 여부를 체크한 후 synchronized block 안에서 한번 더 체크를 한다.
- getInsance( ) 를 호출할 때마다 매번 synchronized 가 걸리지 않으며, 이미 인스턴스가 있는 경우에는 동기화 메커니즘이 동작하지 않아 효율적이다.
- 해당 인스턴스가 필요한 시점에 만들수 있다는 장점을 가질 수 있게된다.
- 단점은 복잡하다. 필드에 volatile 키워드를 따로 주어야만 해당 기법이 완성된다. 왜 volatile 써야하는지를 이해하려면 java 1.4 이하 버전에서 java 가 멀티쓰레드 환경에서 메모리를 어떻게 다루었는지까지도 이해하고 있어야한다.
public class Settings { private static volatile Settings instance; private Settings() {} public static Settings getInstance() { if (instance == null) { synchronized (Settings.class) { if (instance == null) { instance = new Settings(); } } } return instance; } }
[방법 4]
- static inner 클래스 사용하기
- 이 방법은 권장하는 방법 중 하나이다.
- 멀티 쓰레드 환경에서도 안전하고, LazyLoading 도 가능하다.
- double chehked locking 처럼 복잡한 이론 배경을 알 필요도 없다.
package singleton; public class Settings { private Settings() {} private static class SettingsHolder { private static final Settings INSTANCE = new Settings(); } public static Settings getInstance() { return SettingsHolder.INSTANCE; } }
문제는 위 방법을 깨트릴 수 있는 다양한 방법이 존재한다.
[방법 1] 리플렉션
public class Main { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Settings settings = Settings.getInstance(); // [방법1] // 리플렉션을 사용하여 new 연산자를 이용한것과 같이 인스터스를 생성했다. Constructor<Settings> constructor = Settings.class.getDeclaredConstructor(); constructor.setAccessible(true); Settings settings2 = constructor.newInstance(); // singleton 이 깨진것을 확인 할 수 있다. System.out.println(settings.equals(settings2)); // fase System.out.println(settings == settings2); // fase } }
[방법 2] 직렬화 역직렬화
public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { // [방법2] // 직렬화 & 역직렬화 // 자바는 오브젝트를 파일 형태로 디스크에 저장해뒀다가(직렬화) 다시 읽어 드릴수 있다.(역직렬화) // 파일로 저장해뒀다가 로딩할 수 있다는 뜻이다. Settings settings = Settings.getInstance(); Settings settings2 = null; // 1) 직렬화 try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) { out.writeObject(settings); // 여기서 객체가 파일에 써진다. } // 2) 역직렬화 try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) { settings2 = (Settings) in.readObject(); } // 역직렬화를 하게되면 생성자를 사용해서 새로 인스턴스를 만들어준다. System.out.println(settings == settings2); // false // 하지만 대응 방안이 있다. System.out.println(settings == settings2); // true } }
- Serializable 과정을 거친 인스턴스는 싱글톤으로 유지가 되지 않는다.
- 이는 대응 방안이 존재한다.
- 역직렬화 시 readResolve( ) 메서드가 내부적으로 호출되는데, 이때 임의로 이전에 만들어진 인스턴스가 호출되도록 설정하면 된다.
public class Settings implements Serializable { public Settings() {} private static class SettingsHolder { private static final Settings INSTANCE = new Settings(); } public static Settings getInstance() { return SettingsHolder.INSTANCE; } // 역직렬화 대응 방안. // 역직렬화 시 해당 메서드가 자동으로 호출되는데, 원래는 Settings 인스턴스를 내려주게된다. // 하지만 우리는 임의로 원래의 인스턴스가 내려가도록 설정해두어 singleton 을 지킬 수 있다. protected Object readResolve() { return getInstance(); } }
리플렉션과 직렬화/역직렬화를 이용하여 싱글톤을 깨트리는것 까지 막고싶은 경우엔 Enum 을 사용하자.
- enum 으로 singleton 구현시 리플렉션(Reflection)에 안전한 코드가 된다.
- enum 은 리플렉션에서 new 인스턴스를 사용 할 수 없도록 막아놨기 때문이다.
- enum 에서도 생성자, 프로퍼티, 메서드 모두 구현이 가능하다.
- 직렬화 / 역직렬화를 통해 인스턴스 생성 시에도 안정적이다.
- enum 은 기본적으로 Serialize 를 구현하고 있는데 이때 같은 인스턴스가 반환되도록 설정되어있다.
- 단점은 클래스를 로딩하는 순간 미리 만들어진다는 점과 상속을 사용하지 못한다.
- 그것이 크게 문제가 되지 않는다면, 가장 완벽한 방법일 수 있다.
package Singleton; public enum Settings { INSTANCE; }
1. 자바에서 enum을 사용하지 않고 싱글톤을 구현하는 방법은?
- 생성자를 private 으로 설정한 다음, 인스턴스를 생성할 메서드를 하나 구현한다.
이때 인스턴스를 생성해주는 메서드는 public static 으로 설정하여 외부에서 전역 메서드로 호출되어질수 있게끔 설정한다. - 위 방법과 동일하게 생성하지만 synchronized 키워드 설정하여 멀티쓰레드를 막는 방법
- 이른 초기화 방법 (이는 static 한 필드들이 초기화 되는 시점에 인스턴스가 생성되도록 하는 방법이다. 멀티쓰레드를 막는 방법)
- double checked locking 방법 (인스턴스가 없는 경우에만 synchronized 가 걸린다.)
syncronized block 을 설정하여 해당 block 전/후로 인스턴스 존재 여부를 확인한 후 인스턴스가 생성되도록 한다. - static inner 클래스를 사용하는 방법 (권장)
2. private 생성자와 static 메소드를 사용하는 방법의 단점은?
- 멀티쓰레드 환경에서 안전하지 않을 수 있다.
- 리플렉션과 직렬화&역직렬화를 이용하여 새로운 인스턴스 생성이 가능해진다.
3. enum 을 사용해 싱글톤 패턴을 구현하는 방법의 장점과 단점은?
- 리플렉션과 직렬화&역직렬화를 이용했을때 새로운 인스턴스 생성을 막을 수 있다. (장점)
- 즉시 로딩(eager loading) 이 되어 인스턴스가 미리 만들어진다. (단점)
- 상속을 사용하지 못한다. (단점)
4. static inner 클래스를 사용해 싱글톤 구현
package singleton; public class Settings { public Settings() {} private static class SettingsHolder { private static final Settings INSTANCE = new Settings(); } public static Settings getInstance() { return SettingsHolder.INSTANCE; } }
싱글톤 패턴, 실무에선 어떻게 쓰일까?
- 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프
- 자바 java.lang.Runtime
Runtime runtime = Runtime.getRuntime(); System.out.println(runtime.maxMemory()); System.out.println(runtime.freeMemory());
- 다른 디자인 패턴 구현체의 일부로 쓰이기도 한다.
- Builder Parttern
- Facade Parttern
- Abstract Factory Parttern
'디자인 패턴' 카테고리의 다른 글
프로토타입 (Prototype) 패턴 (0) 2022.06.29 추상 팩토리 (Abstract factory) 패턴 (0) 2022.06.19 팩토리 메소드 패턴 (Factory Method Pattern) (0) 2022.06.14 프록시(proxy) 패턴 (0) 2022.05.03 발행-구독 패턴 (0) 2022.04.30