디자인패턴

[디자인패턴] 생성 패턴 - 싱글톤(Singleton) 패턴

swimKind 2022. 8. 23. 22:55

[디자인패턴] 생성 패턴 - 싱글톤(Singleton) 패턴

 

 

*싱글톤 패턴

- 인스턴스를 오직 한개만 제공하는 클래스

  • 인스턴스가 여러개 있을때 문제가 발생할 수 있는 경우를 대비해 한개의 인스턴스만 제공하는 클래스가 필요하다.

 

 

- 기본적인 싱글톤패턴

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if(instance == null){
      instance = new Singleton();
    }
    
    return instance;
  }
}
public class App {
 public static void main(String[] arg){
   Singleton singleton = Singleton.getInstance;
   System.out.println(singleton == Singleton.getInstance); // true
 }
}

위와 같은 경우는 하나의 싱글톤 인스턴스를 반환하지만 문제가 있다. 웹 애플리케이션 같은 멀티스레드 환경에선 이 코드가 안전하지 않다.(Thread-Safe 하지 않다)

 

멀티스레드 환경에서 안전하려면 다음과 같은 방법으로 구현할 수 있다.

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static synchronized Singleton getInstance() {
    if(instance == null){
      instance = new Singleton();
    }
    
    return instance;
  }
}

synchronized 키워드를 사용하게되면 여러 스레드에서 getInstance 메소드에 동시에 접근할 때, Thread-Safe하게 접근할 수 있다. 멀티 스레드 환경에서 synchroinzed 키워드를 만났을 때, 어떤 스레드가 해당 부분에 진입한 상태라면 다른 스레드는 그 스레드의 작업을 끝날 때까지 대기한다. 하지만 synchronized는 내부적으로 동기화라는 매커니즘으로 locking 작업을 하기때문에 성능적은 불이익이 생길 수 있다.

 

 

 

그래서 성능을 조금 더 신경쓰고 싶다면 다른 방법을 사용할 수도 있다.

public class Singleton {
  private static final Singleton INSTANCE = new Singleton();
  
  private Singleton() {}
  
  public static Singleton getInstance() { 
    return INSTANCE;
  }
}

synchronized를 사용하지 않고 인스턴스를 먼저 생성한다. 이른 초기화라고 하는데 생성자를 먼저 생성한 후에 getInstance 메서드는 생성된 인스턴스만을 반환한다. Thread-Safe하며 synchronized 동기화에 관한 성능이슈도 없지만 미리 만드는것 자체가 이슈가 될 수 있다.

생성하는 인스턴스 자체로 메모리에 할당을 하게 되는데 만약 미리 만들어졌지만 애플리케이션 내에서 사용하지 않는다면 ? 이것 또한 자원의 낭비가 될 것이다.

 

 

그래서 또 다른 방법이 있다.

public class Singleton {
  private static volatile Singleton instance; // volatile java 1.5 버전 이상부터 사용
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if(instance == null) {
      synchronized (Singleton.class) {
        if(instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

synchronized 키워드를 사용하긴 하지만 메서드 레벨에서 사용하는 것이 아닌, 메서드 안의 로직에서 사용한다. double checked locking 이라는 기법으로 if 문을 통해서 2번의 체크하는 로직을 가진다.

여러 스레드가 동시에 getInstance 메서드에 진입하더라도 if문에서 한번 필터링을 하고(동기화 매커니즘까지 가지 않음) synchronized 블럭 안에서 한번 더 if로 필더링을 하기 때문에, 메서드에서 synchronized를 사용한 경우처럼 메서드를 호출할 때마다 동기화 locking 작업을 하지 않는다.

하지만, 이 방법은 복잡하다. 필드에 선언된 volatile을 사용한 이유는 자바 1.5버전 이하(자바 1.4부터)의 메모리처리 방법을 이해 해야한다.

 

 

따라서 또 다른 방법이 있다.

public class Singleton {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
}

이 방법은 권장하는 방법중 하나이다. 싱글톤을 static inner 클래스를 만들어서 구현하는 방법이다.

이 방법은

1) Thread-safe하며

2) double check locking처럼 복잡하지도 않고

3) getInstance를 호출할 때 클래스 로딩을 통해서 인스턴스를 생성하기 때문에 lazy loading도 가능하다.

 

 


 

 

이러한 방법들은 싱글톤을 보장하며 멀티스레드 환경에서 적절한지, 성능상으로도 괜찮은지, 버전에 따른 복잡한 절차가 들어 있는지에 대한 문제에 대해 자유로울 수 있다.

하지만, 이러한 싱글톤의 구현을 깨뜨리는 방법 또한 존재한다. 사용자가 정해진 방법이 아닌 다른 방법으로 사용한다면 이러한 싱글톤을 보장하지 않을수도 있다.

  • 리플렉션
  • 직렬화와 역직렬화

이 두가지의 방법을 이용하면 위의 싱글톤을 깨뜨린다.

 

 

- 리플렉션

public class App {
  public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessable(true); // private 접근
    Singleton singleton1 = constructor.newInstance();
    
    System.out.println(singleton == singleton1); // false
  }
}

우리가 의도한대로 만든 싱글톤과 리플렉션을 사용하여 클래스에 접근하여 만들어낸 생성자는 다른 인스턴스를 만들어낸다.

 

 

- 직렬화 & 역직렬화

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;
  }
}
public class App {
  public static void main(String[] args) throws IOException {
    Singleton singleton = Singleton.getInstance();
    Singleton singleton1 = null;
    
    try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) {
      output.writeObject(singleton); // 직렬화
    }
    
    try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) {
      singleton1 = (Singleton) in.readObject(); // 역직렬화
    }
    
    System.out.println(singleton == singleton1); // false
  }
}

리플렉션과 마찬가지로 직렬화 & 역직렬화를 통해 만든 객체 인스턴스는 싱글톤으로 만들어낸 인스턴스와 다른 객체를 참조한다.

 

* 역직렬화 대응방안

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();
  }
}

명시적으로 readResolve 메서드가 있는건 아니지만 이 메서드는 역직렬화 될때 사용된다. 그래서 readResolve 메서드의 구현을 getInstance 메서드를 반환하게 바꿔주면 동일한 인스턴스를 얻을 수 있다.

 

 

 

권장하는 방법중 또 하나는 enum 타입으로 구현하는 방법이 있다.

public enum Singleton {
  INSTANCE;
}

enum 타입으로 싱글톤을 구현하게 되면 class로 구현하게 되어 생기는 리플렉션으로 싱글톤이 깨지는 현상에 대응 할수 있다. 

 

public class App {
  public static void main(String[] args) {
    Singleton singleton = Singleton.INSTANCE;
    Singleton singleton1 = null;
    
    Constructor<?>[] constructors = Singleton.class.getDeclaredConstructors();
    for (Constructor<?> constructor: constructors) {
      constructor.setAccessable(true);
      singleton1 = (singleton) constuctor.newInstance("INSTANCE"); // cannot reflectively create enum objects
    }
    
    System.out.println(singleton == singleton1);
  }
}

enum은 리플렉션에서 new를 통한 생성자를 만들수 없다. 따라서 유일한 인스턴스가 보장이 된다. enum은 역직렬화나 리플렉션과 같은 이탈행위를 방지할 수 있다. 하지만 단점은 enum은 클래스를 로딩하는 순간 미리 인스턴스가 만들어진다. 또한 enum은 오로지 enum만 상속 받을 수 있기때문에 다른 class를 상속 받을 수 없다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com