m1ndy5's coding blog

싱글톤(Singleton) feat. 스프링 본문

백엔드 with java/Design Pattern

싱글톤(Singleton) feat. 스프링

정민됴 2023. 12. 4. 20:35

싱글톤 정의

생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴하는 것
즉 인스턴스가 오직 한개라는 뜻이다.

고전적인 싱글톤 패턴 구현

public class Singleton{
    private static Singleton uniqueInstance;

    private Singleton(){}

    public static Singleton getInstance(){
        if (uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

생성자를 private로 선언해 외부에서 인스턴스 생성을 막고
static 변수로 Singleton을 선언하여 클래스 변수로 한개만 만든다.
싱글톤 객체가 필요하다면 getInstance() 함수를 호출하여 가져오면 된다.

싱글톤의 문제점

만약에 멀티스레드 환경에서 각각의 스레드가 거의 동시에 싱글톤을 호출했다고 하자.
1번 스레드에서 인스턴스를 만들기도 전에 2번 스레드에서 인스턴스를 불러오게 되면 인스턴스가 2개 만들어지는 불상사가 발생
이런 불상사를 막으려면

public class Singleton{
    private static Singleton uniqueInstance;

    private Singleton(){}

    public static synchronized Singleton getInstance(){
        if (uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

synchronized 키워드를 사용하여 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드가 기다릴 수 있게 하면 된다.
하지만 이런 경우 속도가 느려지고 메소드가 시작될 때만 동기화되면 되는데 동기화가 계속 유지되어 성능저하로 이어질 수 있다.

효율적으로 멀티스레드에서의 문제 해결

방법 1.
getInstance()의 속도가 그렇게 중요하지 않다면 그냥 두기
그냥 synchronized 키워드를 붙여서 사용하는 것도 속도가 그렇게 중요하지 않다면 좋은 방법이다.
하지만 getInstance()가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해봐야한다.
방법 2.
인스턴스가 필요할 때는 생성하지말고 처음부터 만들기

public class Singleton{
    private static Singleton singleton = new Singleton();

    private Singleton(){}

    public static Singleton getInstance() {
        return singleton;
    }
}

이렇게 되면 static initializer에서 singleton의 인스턴스를 처음부터 생성하기 때문에 한개만 생긴다.
방법 3.
DCL을 사용해서 getInstance()에서 동기화되는 부분을 줄이기
DCL(Double-Checked Locking)을 사용하면 인스턴스가 생성되어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화가 가능하다.
즉 처음에만 동기화하고 다음부턴 안한다.

public class Singleton{
    private volatile static Singleton singleton;

    private Singleton() {}

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

volatile 키워드를 사용하면 멀티스레드를 사용하더라도 초기화되는 과정이 올바르게 진행된다.
하지만 싱글톤은 이 외에도 리플렉션, 직렬화, 역질렬화에서도 문제가 될 수 있다.
가장 좋은 방법은

public enum Singleton{
    UNIQUE_INSTANCE;
    // 기타 필요한 필드
}
public class SingletonClient{
    public static void main(String[] arges){
        Singleton singleton = Singleton.UNIQUE_INSTANCE;
    }
}

이런식으로 ENUM을 사용해서 나타내는 방법이다.

싱글톤과 스프링

싱글톤은 스프링에서도 아주 중요하게 쓰이는 개념이다.
바로 스프링 컨테이너가 객체 인스턴스들을 싱글톤으로 관리해주기 때문이다.
ex) 여러 클라이언트가 memberService를 요청해도 여러 memberService구현체를 만들지 않고 스프링 컨테이너에서 하나의 memberService 구현체에서 응답하도록 관리함

싱글톤 방식의 주의점

여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 stateful하게 설계하면 안된다.
다음은 stateless하게 설계하는 방법이다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야한다.

ex) 사용자가 싱글톤의 변수를 A로 설정했는데 B가 뒤따라서 B로 변경했다고 가정했을 때 A가 조회를 해봤더니 B가 나오는 띠용하는 상황이 생길 수도 있다.
실생활적인 예로들면 내가 다른사람의 주문내역이나 결재내역을 확인하게 되는 큰 오류가 날 수 있다!!

어떻게 스프링 컨테이너가 스프링 빈을 싱글톤으로 관리할까?

바로 @Configuration 어노테이션을 사용하면 해당 설정파일 클래스를 불러오는 것이 아닌 설정파일 클래스를 상속받은 클래스를 불러오게 된다.
이 때 이미 스프링 컨테이너에 호출된 스프링빈이 등록되어있으면 만들지 않고 기존 애를 반환한다.
@Congifuration을 사용하지 않고 @Bean만 사용해도 스프링 컨테이너에 등록은 되겠지만 @Configuration어노테이션 없이는 싱글톤을 보장하지 않는다.