아키텍쳐

싱글톤 패턴

qpmi1zm29 2023. 12. 27. 20:02

어플리케이션을 운영하면서 사용자 요청 별로 생성이 되어야 하는 클래스도 존재하지만, 

딱 하나의 인스턴스만 생성하여 사용해야 하는 클래스가 있다. 이런 경우 싱글톤 패턴을 적용하여 구상한다.

 

싱글톤 클래스는 외부에서 객체의 생성을 막아야 하며, 외부에서 객체를 사용하기 위해 제공할 수단이 필요하다.
그러므로, private 생성자를 갖고, static으로 인스턴스를 제공하는 메소드가 필요하다.

 

멀티 스레드 환경에서 Thread-safe하게 인스턴스를 외부로 제공하기 위해서 사용되는, 4가지 구현 방법이 존재한다.

이는 제공되는 인스턴스의 내부 필드에 대해서 thread-safe 한 것은 아니다. 
클래스를 불변 클래스로 구성하거나, 변경되지 않는 데이터일 경우 필드와 메소드까지 final로 정의한다. 

 

synchronized 키워드 사용
public class A {

    private static A instance;

    private A() { }

    public static synchronized A getInstance() { // 클래스 lock 사용
        if (instance == null) {
            instance = new A();
        }

        return instance;
    }

}

 

 

더블 체크 라킹 메카니즘 사용
public class A {

    private static volatile A instance; //자바 1.5 이상부터 volatile 키워드가 필수.

    private A() { }

    public static A getInstance() {
        if (instance == null) {
            synchronized (A.class) {      // 클래스 lock 사용, static 메서드이기 때문에
                if (instance == null) {
                    instance = new A();
                }
            }
        }

        return instance;
    }

}

 

메소드 레벨에 synchronized 키워드를 작성한 것보다 부하가 적다. instance 변수가 null이 아닐 경우에는 임계영역으로 진입하지 않기 때문이다.

 

위 두가지 방법은 실제 A 클래스의 인스턴스를 사용하고자 할 때 객체를 할당하므로 lazy initialization이라고 볼 수 있다.

그래서 만약 객체 생성 시 비용이 많이 드는 클래스가 있다고 할 때, 불필요하게 리소스를 낭비하는 경우를 배제할 수 있다.

 

 

이른 초기화

 

클래스를 메모리에 로드할 때부터 클래스 변수를 초기화 하는 방법이다.

 

public class A {

    private static final A INSTANCE = new A();

    private A(){}

    public static A getInstance() {
        return instance;
    }
   
}

 

이 방법은 간단하고 Thread-safe 하다. 그러나 사용 여부에 상관 없이 객체를 생성한다는 단점이 있는데 이를 보완한 방법이 내부 클래스를 사용하는 방법이다.

 

만약에 생성자에서 checked 예외를 던지게 된다면 어떻게 처리해야 할까 ??
- try - catch 문으로 예외 처리를 해주어야 한다. 
생성자 메소드에서 예외가 발생하면, 사용 중이던 리소스들을 모두 해제하는 방향이 가장 best practice 이다.
리소스 해제가 번거로울 수 있어, 반드시 생성자에서 코드 작업 전에 전달 받은 리소스의 유효성을 먼저 확인하여야 한다.

 

 

내부 클래스 사용

 

public class A {

    private A(){};

    public static A getInstance() {
        return AInstanceHolder.INSTANCE;
    }

    private static class AInstanceHolder {
        private static final A INSTANCE = new A();
    }
   
}

 

이 방법이 왜 지연 초기화 방법이냐면, 클래스 로더가 A class를 메모리로 로드 할 때 내부에 정의된 inner 클래스까지 로드하지 않는다. 

내부의 static 클래스는 실제 해당 클래스의 필드가 참조되거나 메소드가 호출되었을 때, 메모리에 클래스의 메타 데이터가 로드된다. 

그러므로 외부에서 A.getInstance()를 호출해야지만 INSTANCE 변수가 초기화 되는 것이다.