코딩하는 개굴이

[디자인 패턴] 싱글턴 패턴 (SingleTon Pattern) 구현하기 본문

기본기 채우기/디자인 패턴

[디자인 패턴] 싱글턴 패턴 (SingleTon Pattern) 구현하기

개굴이모자 2022. 6. 18. 16:49
반응형
GOF_Design_Pattern

해당 내용은 Inflearn 의 코딩으로 학습하는 GoF의 디자인 패턴 강좌를 기반으로 정리되었음을 알립니다.
해당 내용은 주로 JAVA 기반으로 코드 설명이 진행됨을 알립니다.

객체 생성 관련 디자인 패턴

SingleTon Pattern (싱글턴 패턴)

  • 목적
    • 어떤 클래스의 인스턴스를 오직 하나만 만들어서 제공
    • 하나의 인스턴스를 글로벌하게 접근할 수 있는 방법을 제공

아래처럼 new 를 사용하여 생성자를 사용하면, 인스턴스가 여러개 생성이 가능하다.

Sample sample1 = new Sample();
Sample sample2 = new Sample();
boolean isSame = (sample1 == sample2); //false

구현 방법

기본적인 구현

따라서, 생성자를 private 로 생성하여 밖에서 new를 할 수 없게 한다.
그리고, 밖에서 Sample을 가져오기 위해서는 getInstance() 라는 메서드를 통해서만 가능하도록 이를 public static 하게 만들고,
instance 라는 전역 변수를 두어, getInstance 호출 시, Null일 경우에만 새로 생성하고, 아닐 경우에는 생성되어있는 것을 리턴하도록 한다.

public class Sample {
    private static Sample instance;

    private Sample() {...}
    
    public static Sample getInstance() {
        if (instance == null) {
            instance = new Sample();
        }
        return instance;
    }
} 
...
Sample sample1 = Sample.getInstance();
Sample sample2 = Sample.getInstance();
boolean isSame = (sample1 == sample2); //true

위의 방법은 ThreadSafe 하지 않다는 큰 위험을 안고가는데, 멀티스레드인 한 경우를 예시로 들어보겠다.
만일 A라는 Thread 와 B라는 Thread가 거의 동시에 getInstance를 호출하였다고 가정했을 때, A가 getInstance 에 들어와 null이 아님을 확인하고 new를 하기 직전에 B가 null이 아님을 확인하면, 두개의 객체가 생성될 수 있다.

Synchronized 키워드 사용

따라서, 이와 같은 현상을 해결하기 위해서 getInstance 에 synchronized 키워드를 사용하여 해당 메서드에 한번에 한 Thread 만 들어올 수 있도록 메서드를 lock 할 수 있다. 그러나, synchronized 를 사용하므로써 getInstance 를 호출할때마다 별도의 처리가 필요하는 등 성능상 및 코드상의 손해가 발생할 수 있다.

public static synchronized Sample getInstance() {...}

이른 초기화 (eager initialization)

다른 해결 방법으로는 초기에 생성하는 비용이 크지 않다라고 한다면 내부에서 Null을 확인하는 절차를 없애기 위해 초기에 초기화를 해버릴 수 있다.
해당 방법은 ThreadSafe 하지만 초기에 생성하는 행위 자체가 경우에 따라 단점이 될 수 있다. 예를들어, 해당 클래스를 생성하기 위해서는 메모리를 많이 필요로하는 등의 상황에서 생성했음에도 사용하지 않는 경우에는 손해가 된다.

public class Sample {
    private static final Sample INSTANCE = new Sample();

    private Sample() {...}
    
    public static Sample getInstance() { 
        return INSTANCE;
    }
}

Double Checked Locking 사용

그렇기에 사용하는 시점에서만 생성하는 것을 원할 수 있다.
그럴 경우에, double checked locking 으로, 효율적인 동기화 블럭을 만들 수 있다.
아래처럼 한번 null 체크 후에 synchronized 블럭을 타게 만들어, 만일 다른 스레드가 블럭 안에 있다면, 기다렸다가 들어가게되고 생성되었으면 있는 instance 를 반환하는 방법이다.

여기에서 왜 instance 가 null인지 2번이나 체크하는지 궁금할 수 있다. 이는 효율성 때문인데, synchronized 블럭 부분만 존재한다면, Thread 가 이미 내부에 있는 경우에는 매번 기다리는 비효율적인 동작이 될 것이고, 만일 블럭 내부의 분기가 없다면, 밖에서 체크하고 블럭을 탈때 무조건 생성하기 때문에 다시 ThreadSafe하지 않다.

public class Sample {
    private static volatile Sample instance; //Java 1.5 이상부터 동작하는 키워드 volatile

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

단, 위의 방법은 Java 1.5 이상부터만 사용 가능하며 다소 복잡한 방법 중 하나이다.

Static Inner 클래스 사용 (권장)

따라서, 멀티스레드 환경에서도 안전하고 사용할때만 생성될 수 있는 방법으로 권장되고 있는 방법은 아래와 같다.

public class Sample {
    private Sample() {...}

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

enum 사용

싱글턴을 깨트리다..?

이렇게 여러 고민을 하여 싱글턴 패턴으로 생성하였다고 하더라도, 사용하는 과정에서 깨트려지는 경우가 발생할 수 있다.

직렬화 & 역직렬화

해당 방법으로 깨트리려면, 우선 생성한 Sample class 가 Serializable을 상속하고 있을 경우에 가능하다.
이는, 파일로 쓰고(직렬화) 다시 불러오는 것(역직렬화)이 가능해진다는 의미인데
역직렬화 시에는 반드시 생성자를 이용해 다시 인스턴스를 만들어주기 때문에, 이를 이용해 아래와 같이 싱글턴을 깨트리는 구현이 가능하다.

Sample sample1 = Sample.getInstance();
Sample sample2 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("sample.obj"))) {
    out.writeObject(sample);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("sample.obj"))) {
    sample2 = (Sample) in.readObject();
}
boolean isSame = (sample1 == sample2); // false
역직렬화 대응 방안

본래 역직렬화시에는 readResolve 라는 메서드가 사용되기 때문에 본래 명시적으로 overriding 이 가능한 것은 아니지만, 아래처럼 해당 시그니쳐를 가지고 있으면 readResolve 가 호출되어 getInstance 를 반환하게 할 수 있다.

public class Sample implements Serializable {
    ...
    protected Object readResolve() {
        return getInstance();
    }
}
리플렉션

getDeclaredConstructor 를 통해 정의된 생성자를 직접 가져오는 방식으로 새로운 Instance 를 강제 생성할 수 있다.

Sample sample1 = Sample.getInstance();

Constructor<Sample> declaredConstructor = Sample.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Sample sample2 = declaredConstructor.newInstance();

boolean isSame = (sample1 == sample2); // false
SingleTon을 깨트리는 것을 막기 위해 enum 사용하기

자바에서는 enum 은 리플렉션에서 new Instance 를 할 수 없도록 막혀있기 때문에, 유일한 인스턴스를 보장 가능하고 직렬화&역직렬화와 리플렉션의 대응이 가능
그러나, enum은 로딩하는 순간 이미 만들어지고, 상속이 불가한 단점이 있다.

public enum Sample {
    INSTANCE;
}

복습

  • 자바에서 enum을 사용하지 않고 싱글턴 패턴을 구현하는 방법?
  • private 생성자와 static 메서드를 사용하는 방법의 단점?
  • enum을 사용해 싱글턴 패턴을 구현하는 방법의 장/단점?
  • static inner 클래스를 사용해 싱글턴 패턴을 구현해보자
반응형
Comments