디자인 패턴

싱글톤 패턴 (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