5. 싱글톤 패턴(Singleton Pattern)

Kim Dong Kyun·2023년 6월 29일
3

Design Pattern

목록 보기
5/5
post-thumbnail

썸네일 출처

싱글턴?

스프링 빈은 싱글턴으로 관리된다.

  • 스프링은 계층형 구조 (MVC)로 이루어진 형태이고, 각각의 레이어들은 다른 역할을 감당하면서 협력한다.

  • 트래픽이 높은 상황, 초당 수백 수천건의 요청이 서버로 오는 경우를 상상해보자.

  • 이 때, 클라이언트에서 요청이 올 때마다, 레이어별로 매번 객체를 새로 만들어서 사용한다면 서버는 매우 큰 부하가 걸릴 것이다.

  • 이처럼 상황에 따라서 객체의 "인스턴스"를 오직 하나로 보장하는 것이 싱글턴 패턴의 기본 아이디어이다.

객체의 인스턴를 오직 하나로 보장하는 방법에는 무엇이 있을까? 함께 알아보자!


객체의 인스턴스를 하나로 보장하기

1. 고전파

public class OldSingleton {
    private static OldSingleton uniqueInstance;

    private OldSingleton() {} // 생성자 매서드

    public static OldSingleton getInstance(){
        if (uniqueInstance == null){
            uniqueInstance = new OldSingleton();
        }
        return uniqueInstance;
    }
}
  • if 분기문을 통해서 필드에 선언한 static한 인스턴스, uniqueInstance가 초기화 되어있는지 확인한다 (초기화 되지 않았다면, null일것이다.)

  • 초기화 되어 있지 않다면 새 인스턴스를 생성한다. 이렇게 인스턴스가 생성되면, 다음 분기부터 해당 인스턴스는 null이 아니므로 분기문을 타지 않는다.

  • 인스턴스를 반환한다. 이 인스턴스는 해당 분기문에 의해 "오직 하나"의 인스턴스임을 보장받는다.

굉장히 쉽습니다. 하지만 이 코드가 괜찮은 코드일까요? 실제 사용 가능할까요?

해당 방식의 문제

멀티 스레드 환경에서 문제가 발생할 소지가 매우 높습니다.

예를 들어, 두 개의 귀여운 스레드가 있다고 합시다. (A,B)

두 스레드가 해당 로직을 거치는 과정을 한번 실험해보죠.

static class SingletonThreadTest {
        public static void main(String[] args) {
            final int numThreads = 5; // 생성할 스레드 수

            Runnable runnable = () -> {
                OldSingleton singleton = OldSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + ": " + singleton);
            };

            Thread[] threads = new Thread[numThreads];

            for (int i = 0; i < numThreads; i++) {
                threads[i] = new Thread(runnable);
                threads[i].start();
            }
        }
    }

귀여운 쓰레드들이 어떤 인스턴스를 생성했나 살펴봅시다.

0번 스레드 혼자 다른 인스턴스를 생성했네요!

이러면 인스턴스가 하나인 것이 보장받지 못합니다.


2. 중세파

고전파의 문제를 해결해봅시다!

방법이 세 가지나 됩니다! 세 방법은 다음과 같습니다.
코드 이후엔 설명을 첨부했습니다.

public class MiddleAgeSingleton {
    private static MiddleAgeSingleton uniqueInstance; // 방법1
    private volatile static MiddleAgeSingleton volatileUniqueInstance; // 방법2
    public static synchronized MiddleAgeSingleton getInstance1(){ // 방법 1
        if (uniqueInstance == null){
            uniqueInstance = new MiddleAgeSingleton();
        }
        return uniqueInstance;
    }
    public static MiddleAgeSingleton getInstance2(){ // 방법2
        if (volatileUniqueInstance == null){
            synchronized (MiddleAgeSingleton.class) {
                if (volatileUniqueInstance == null){
                    volatileUniqueInstance = new MiddleAgeSingleton();
                }
            }
        }
        return volatileUniqueInstance;
    }
    private static final MiddleAgeSingleton readyMadeUniqueInstance 
    = new MiddleAgeSingleton(); // 방법 3
}

방법1

private static MiddleAgeSingleton uniqueInstance; // 방법1

public static synchronized MiddleAgeSingleton getInstance1(){ // 방법 1
        if (uniqueInstance == null){
            uniqueInstance = new MiddleAgeSingleton();
        }
        return uniqueInstance;
    }
  • synchronized 키워드를 써서 스레드를 동기화 시킵니다.

  • 즉, 줄세웁니다. "나 끝나기 전까지는 하지말어!!"

  • 동기화의 문제는 성능입니다. 비동기식 처리와 대략 100배정도의 성능 차이가 있습니다.

따라서 성능상 이슈가 없을 때, getInstance() 매서드가 그리 자주 호출되지 않을 땐 문제가 되지 않습니다만, getInstance()가 병목으로 작용한다면(자주 호출된다면) 문제가 됩니다.


방법2

public class MiddleAgeSingleton {
	private static final MiddleAgeSingleton readyMadeUniqueInstance
 									 = new MiddleAgeSingleton(); // 방법 2
    
    public static MiddleAgeSingleton getInstance2(){
        return readyMadeUniqueInstance;
    }
}
  • 심플한 방법입니다. 미리 인스턴스를 생성해놓고 쓰는 방법!

미리 인스턴스를 생성한다는 것은, 클래스로더에 의해서 클래스가 초기화 되었을 때 인스턴스가 생성된다는 뜻입니다. 불필요한 공간 낭비가 있을 수 있다는 뜻이죠


잠깐! Java 클래스로더 초기화 시점?

클래스 로더(JVM 안에 있음)의 클래스 초기화 시점이 어떻게 되는데요?

  1. 클래스 인스턴스 생성
  • 클래스로더는 클래스의 인스턴스가 처음으로 생성될 때 해당 클래스를 초기화합니다. 즉, new 키워드를 사용하여 클래스의 객체를 생성할 때 초기화가 발생합니다.
  1. 정적 메서드나 정적 변수에 접근
  • 정적 메서드나 정적 변수에 처음으로 접근할 때 해당 클래스를 초기화합니다.
  1. 클래스 초기화 블록
  • 클래스로더는 클래스 초기화 블록(static initializer block)이 실행되는 시점에 해당 클래스를 초기화합니다. 클래스 초기화 블록은 static { } 블록 내에 작성되며, 클래스가 로드되고 링크된 이후에 실행됩니다.

퀴즈! 방법2의 코드는 언제 초기화될까요?

  • new 키워드로 생성되는 "readyMadeUniqueInstance" 의 생성 타이밍에 클래스로더에서 초기화됩니다.

방법3

private volatile static MiddleAgeSingleton volatileUniqueInstance; // 방법3

public static MiddleAgeSingleton getInstance3(){ // 방법3
        if (volatileUniqueInstance == null){
            synchronized (MiddleAgeSingleton.class) {
                if (volatileUniqueInstance == null){
                    volatileUniqueInstance = new MiddleAgeSingleton();
                }
            }
        }
        return volatileUniqueInstance;
    }
  • volatile 키워드를 써서 DCL(Double-Checking-Locking)을 사용하는 것입니다.

  • DCL을 사용하면 인스턴스가 생성되어 있는지 확인 한 다음, 생성되지 않았을 때만 동기화합니다.

  • if문에서 인스턴스를 확인하고, 없다면 동기화된 블록으로 들어갑니다.

if (volatileUniqueInstance == null){
//if문에서 인스턴스를 확인하고, 없다면 동기화된 블록으로 들어갑니다.
            synchronized (MiddleAgeSingleton.class) {
            // synchronized 키워드를 사용해서 동기화합니다
                if (volatileUniqueInstance == null){
                    volatileUniqueInstance = new MiddleAgeSingleton();
                }
            }
        }
  • synchronized 키워드를 사용해서 동기화한 로직을 실행합니다.

  • 로직 안에는 인스턴스의 존재를 더블체킹하는 로직이 존재하며 ( 동기화된 블록 접근 전에 인스턴스가 생겼는지 확인하기 위함 ), 체크되면 인스턴스를 생성합니다.

성능상 이슈가 해결 가능합니다! 적절한 방법이군요.


추천되는 방식!

위 방식들도 좋지만(어떤 건 안좋구요), 이런 방식은 어떨까요?

1. ENUM

public class EnumSingleton {
    public enum SingletonEnum {
        UNIQUE_INSTANCE;
    }

    static class SingletonClient {
        public static void main(String[] args) {
            SingletonEnum enumSingleton = SingletonEnum.UNIQUE_INSTANCE;
        }
    }
}
  • 아주아주 간단한 방법입니다.

  • 이 간단한 방법이

  1. 동기화 문제 (synchronized 키워드의 성능이슈)
  2. 클래스 로딩 문제 (메모리 공간 차지, new Instance)
  3. 리플렉션 ( 새로운 인스턴스를 생성해도 이미 존재하는 유일한 인스턴스와 동일 객체 취급됩니다 )
  4. 직렬화, 역직렬화 문제 (ENUM은 자바에서 직렬화 및 역직렬화 과정을 자동으로 처리합니다. ENUM 인스턴스는 직렬화될 때 JVM에 의해 고유 식별자가 생성 되며, 역직렬화될 때에도 같은 싱글톤 인스턴스가 복원됩니다)

전부 해결 가능합니다.


2. 홀더 방식

public class HolderSingleton {

    private HolderSingleton() {
        // private 생성자
    }

    private static class SingletonHolder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • Holder 클래스가 실제 인스턴스 생성하는 클래스를 품고 있는 형태입니다.

  • HolderSingleton(이름이 구져서 죄송합니다) 클래스의 초기화 시점은 해당 클래스의 정적 변수, 매서드에 접근하는 시점이므로, lazy init이 보장됩니다.

  • 초기화 시 홀더클래스도 함께 초기화되며, 하나의 인스턴스를 보장합니다.


모든 코드는 깃허브에 올려뒀습니다.


"헤드퍼스트 디자인 패턴" 책을 참고했습니다.

2개의 댓글

comment-user-thumbnail
2023년 6월 29일

크...정리가 너무 깔끔하네요!!

1개의 답글