Singleton

yoon·2022년 9월 29일
0

Singleton Pattern

객체의 인스턴스가 오직 1개만 생성되어, memory 상에 1개만 존재하는 패턴을 의미한다. 이렇게 만들어진 인스턴트는 전역적으로 접근이 가능하다. 따라서, concurrency에 주의해야 한다. (singleton 에 접근하고 있는 쓰레드가 여러개면?)

Anti Pattern

  • 테스트 작성의 어려움
    mock 객체를 생성할 수 없어 테스트 코드를 작성하기가 어렵다.
  • 객체 지향의 의도와 맞지 않는다
    싱글톤은 global state 를 만들 수 있는데, 객체 지향 프로그래밍에서 아무 객체나 자유롭게 접근/수정/공유 하는 전역 상태를 갖는 것은 지양되어야 한다.
    또한, private 생성자를 갖고 있어 상속이 불가능하다. 이로 인해 객체 지향의 특징인 다형성을 적용할 수 없다.
  • SOLID 원칙의 위반
    SOLID 원칙을 지키기 위해서는, 인터페이스로 설계 후 세부 구현은 감춰야 한다. 하지만, 싱글톤을 사용하게 되면 인터페이스가 아닌 콘크리트 클래스 객체를 생성한 후 정적 메소드를 사용하게 된다. 이는 싱글톤을 사용하는 곳과 싱글톤 클래스 사이에 의존성을 증가시키고, 테스트 작성과 추후 변경이 있을 때 수정을 어렵게 만든다.
  • 서버 환경에서는 싱글톤이 싱글을 보장하지 못할 수 있다
    생성자가 private 이더라도, reflection 을 통해 하나 이상의 인스턴스를 만들 수 있다. 또한, 여러개의 JVM 에 분산되어 실행할 때에도 각각 독립적인 싱글톤이 생겨 싱글톤의 본 목적으로서의 가치를 할 수 없다.

그럼에도 싱글톤이 때로는 유용한 이유

  • 단일 오브젝트만 존재해야 하며, 이를 어플리케이션 여러 곳에서 존재 하는 경우
    예를 들어, Controller 와 Service 객체가 싱글톤이 아니라면? 클라이언트 요청이 날라올 때 마다 객체가 새로 생성되고, 요청이 증가할 수록 객체 수도 증가해서 부하를 일으킬 것.

스프링 싱글톤

Bean

Bean 은 스프링이 IoC 방식으로 관리하여, 스프링이 직접 생성과 제어를 담당하는 객체이다. 이 때, 스프링은 기본적으로 빈 객체를 모두 싱글톤으로 만든다. (spring bean 의 scope 이 singleton)

Singleton Registry

싱글톤 구현 법으로 발생하는 다양한 문제점을, 스프링은 싱글톤 컨테이너를 제공해서 해결한다. 빈 생성에 대한 제어권이 컨테이너에게 있기 때문에, 싱글톤 객체 생성을 위해 private 생성자를 꼭 사용하지 않아도 된다. 컨테이너가 빈을 싱글톤으로 생성하여, public 생성자를 가진 java 클래스를 싱글톤으로 활용할 수 있다.

Java Singleton 구현

Eager Initialization

가장 간단한 방법이다.
단, 기동 시간에 문제가 발생한다. 클래스 로딩 단계에서 모든 싱글톤 클래스의 인스턴스를 생성하기 때문에, 해당 인스턴스를 사용하지 않더라도 모든 싱글톤이 준비가 되어야 한다. 따라서, demand 될 때 init 하는 것이 필요하다.

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

Lazy Initialization

인스턴스 낭비 문제는 해결된다.
하지만, multi-thread 환경에서 동기화 문제가 발생한다. 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance() 함수를 호출하면, 여러 싱글톤이 생성될 수 있다.

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

Thread Safe Lazy Initialization

synchronized 키워드를 통해, 오직 하나의 쓰레드만 접근 가능하게 한다.
하지만, synchronized 키워드 자체에 대한 비용이 커(lock 을 걸어 blocking 되기 때문에) 싱글톤 인스턴스 호출이 잦을 때에는 성능이 떨어진다.

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

Thread Safe Lazy Initialization + Double-checked Locking

대부분의 언어에서 사용하는 방법.
메소드가 아닌 null 일 경우에만 synchronized.

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

Initialization on Demand Holder Idiom

클래스 안에 클래스(Holder) 를 두어 JVM 에 의존한 방식. Java 의 class loader 가 싱글 쓰레드이기 때문에 thread safe 를 보장할 수 있다.
중첩클래스 SingletonHolder 는 getInstance 메소드가 호출되기 전에는 생성되지 않으며(lazy initialization), final 키워드를 사용해서 다시 값이 할당되지 않도록 한다.

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

Enum initialization

지금까지의 모든 방법은, Java reflection 을 통해 싱글톤이 둘 이상의 인스턴스가 생성되게 할 수 있다.
enum type 은 프로그램 내에서 한번만 초기화되기 때문에, 이를 이용해 싱글톤을 구현한 방법이다. 하지만 역시, eager initialization 문제가 있으며 유연성이 떨어진다는 한계가 있다.

public enum SingletonEnum {
	INSTANCE;
    
    public static void execute() {
    	// ...
    }
}

0개의 댓글