♾️ Computer Science/디자인 패턴

[Design Patterns] 싱글톤 패턴 (Singleton Pattern)

nerowiki 2024. 4. 21. 15:00
728x90

싱글톤 패턴이란

불필요한 인스턴스 생성 없이 오직 한 개의 인스턴스만 생성하여 사용되는 디자인 패턴입니다.

클라이언트들은 항상 같은 객체와 작업하고 있다는 사실을 인식조차 못 할 수 있습니다.

 

싱글톤 패턴을 사용하는 이유

1. 단일 인스턴스를 보장합니다.

최초 한번의 new 연산자를 통해 고정된 메모리 영역을 사용하여 메모리 낭비를 방지합니다.
이미 생성된 인스턴스를 활용하면서 속도 측면에서 장점을 가지고 있습니다. 

 

2. 전역 액세스 지점을 제공합니다.

싱글톤 인스턴스는 전역적으로 접근 가능한 유일한 지점입니다.
Application 어디에서도 이 인스턴스에 접근할 수 있어 데이터 공유와 통신에 용이합니다.
전역 변수는 편리하지만 모든 코드가 잠재적으로 해당 변수 내용을 덮어쓸 수도 있고,
그로 인하여 앱에 오류가 발생해 충돌이 일어날 수도 있으므로 안전한 방법이 아닙니다.

 

싱글톤 패턴의 문제점

싱글톤 패턴은 위 두가지 특성으로 얻게 되는 이점과 반대로 다음의 trade-off 들을 잘 고려해야 합니다.

 

1. 구현하는데 필요한 코드 자체가 많습니다.

정적 메소드에서 객체 생성을 체크하고 생성자를 호출하는 경우
멀티스레드 환경에서 발생할 수 있는 동시성 문제 해결을 위해 syncronized 키워드를 사용합니다.

 

2. 유닛 테스트 하기가 어려울 수 있습니다.

싱글톤 인스턴스는 자원을 공유하고 있기 때문에 테스트가 결정적으로 격리된 환경에서 수행되려면
매번 인스턴스의 상태를 초기화 시켜주어야 합니다.
많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존합니다.
싱글톤 클래스 생성자는 private 상태이며 대부분 언어에서 정적 메서드를 오버라이딩하는게 불가능합니다.

 

3. 의존 관계 상 클라이언트가 구체 클래스에 의존하게 됩니다.

new 키워드를 직접 사용해 클래스 안에서 객체를 생성하고 있으므로,
이는 SOLID 원칙 중 DIP를 위반하게 되고, OCP 원칙을 위반할 가능성도 높습니다. 

그밖에 자식 클래스를 만들 수 없고, 내부 상태를 변경하기 어렵다는 점 등 여러 문제점이 존재합니다.

 

다양한 싱글톤 패턴 구현 방식

Lazy initialization

이른 초기화 싱글톤 패턴으로 private 생성자와 static 메소드를 사용한 가장 보편적인 방식입니다.
public class Singleton {     
	private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {                
    	if (instance == null) {            
        	instance = new Singleton();        
        }                
    	return instance;    
    }
}
멀티 쓰레드 환경에서 취약하다는 문제점이 있습니다.

 

synchronized keyword

Lazy Initialization 코드에 synchronized 를 추가하여 멀티 스레드 환경에서 안전하게 구현합니다.
쓰레드가 synchronized 되어있는 곳에서 작업 중이라면 다른 쓰레드가 접근 못하게 lock을 걸어줍니다. 
public class Singleton {     
    private static Singleton instance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {                
    	if (instance == null) {            
        	instance = new Singleton();        
        }                
    	return instance;    
    }
}
매번 인스턴스를 리턴 받을 때마다 쓰레드를 동기화하여 성능 저하가 발생합니다.

 

Eager initialization

이른 초기화 싱글톤 패턴으로 멀티 쓰레드 환경에서 유발되는 모든 문제를 해결합니다.
쓰레드가 getInstance()를 호출하는 시점이 아닌 Class가 로딩되는 시점, 즉 static 영역의 데이터 로딩 시점에
인스턴스를 생성하기 때문에 하나의 인스턴스만 생성되는 것을 보장해줍니다.
public class Singleton {     
    private static Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {                               
    	return instance;    
    }
}
인스턴스를 미리 만들어놓기 때문에 인스턴스를 사용하지 않는다면 메모리 낭비에 불과합니다.

 

Double Checked Locking

DCL 싱글톤 패턴은 메소드 레벨에 synchronized 가 없고 메소드 내부에 구현되어 있습니다.
인스턴스가 이미 존재한다면 synchronized를 사용하지 않기 때문에 성능 이슈가 없습니다.
public class Singleton {
    private volatile static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (instance == null) {
        	synchronized (Singleton.class) {
            	if (instance == null) {
                	instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
JAVA 1.5 이상부터 동작하는 volatile 키워드를 사용해야만 구현할 수 있는 한계가 존재합니다.

 

Lazy Holder

Lazy Holder 싱글톤 패턴은 현 시점 가장 완벽하다고 평가 받는 방법입니다.
volatile 이나 synchronized 같은 키워드가 없어도 Thread-safe 하면서 성능 또한 보장합니다.
Lazy Initialization 방식을 가져가면서 쓰레드 간 동기화 문제도 동시에 해결합니다.
public class Singleton {
    private Singleton() {}
    
    // inner class
    private static class SingletonHolder {
    	private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
    	return SingletonHolder.INSTANCE;
    }
}

 

결론

싱글톤 패턴은 프레임 워크의 도움 없이 사용하게 된다면 위 글의 장 단점 trade-off를 잘 고려해서 사용해야 합니다.
일반적으로 단독으로 사용될 경우 안티 패턴으로 불릴 만큼 객체 지향에 위배되는 사례가 많으므로,
스프링 컨테이너와 같은 프레임워크의 도움을 받아 장점의 혜택을 안전하게 누리는게 좋습니다.

 

 

참조

더보기