ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 싱글톤 패턴 (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
Designed by Tistory.