본문 바로가기

Java/디자인패턴

디자인 패턴 - 싱글톤 패턴 (Singleton pattern)

반응형

클래스의 인스턴스가 하나만 만들어지고, 그 인스턴스에 접근하기 위한 패턴

 

싱글톤 패턴으로 얻을 수 있는 이점

  • 전역으로 사용할 수 있어 인스턴스를 사용하기 용이
  • 최초 생성된 객체를 재활용이 가능하여 메모리 절약

싱글톤 패턴의 중요한 점은 어떤 환경에서든 인스턴스가 하나만 존재해야한다는 것이다.

이번에는 싱글톤 패턴을 구현하는 여러가지 방법에 대해 쌓아 보겠다.

 

1. 일반적인 싱글톤 패턴 (Lazy initialization)

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

외부에서 직접 호출이 불가능하도록 private로 Singleton을 선언하였고(생성자도) 클래스의 유일하게 Singleton 객체를 Return 하는 메서드를 만들었다. 

최초 Instance가 생성되는 시점은 getInstance() 메서드를 최초 호출할 때이다.

 

이처럼 JVM 혹은 Application uptime때 인스턴스를 생성하지 않고 lazy initialize으로 처리할 수 있다.

위 방법의 싱글톤 방식은 단일 스레드 프로그램에서 최초 구동 시에 부담을 주지 않고자 처리할 때 유용하게 사용 가능할 것으로 보인다.

말한 대로 단일 스레드 프로그램이 아닌 멀티스레드 환경에서는 인스턴스 중복 생성 및 오 사용이 발생할 수 있는 문제점이 있다.

 

2. 멀티스레드에서도 안전한 싱글톤패턴 

2-1. 스레드 안전한 늦은 초기화 (Thread safe Lazy initialization)

그리하여 인스턴스의 동기화를 위해 두 스레드가 getInstance 메서드를 동시에 실행시키지 않도록 synchronized keyword로 처리할 수 있다.

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

instance를 호출하는 메서드에 동기화를 시키면 멀티스레드에서 안정적으로 사용할 수 있다. 

하지만! 싱글톤 패턴에 synchronized 함수로 처리하는 것은 멀티스레드 환경에서 사용하기에 적합하지 않다.

싱글스레드로 처리되는 단순한 프로그램이라면 위 코드처럼 처리해도되지만 멀티스레드의 경우, 싱글톤에 synchronized 함수가 많을 수록 병목현상을 겪게 되므로, 멀티스레드로 구현되어도 결국에는 싱글스레드처럼 동작하게 되는 것이다.

 

개선해보자.

 

2-2. 이른 초기화 방식 (Eager initialization) 

Singleton class 를 compile time에 생성되도록 한 인스턴스를 사용한다.

public class Singleton{
	// 정적 초기화 시 인스턴스 생성
	private static Singleton instance = new Singleton();
	
	private Singleton(){}
	
	public static synchronized Singleton getInstance(){
		return instance;
	}	
	
}

해당 방법으로 다수의 인스턴스가 생성되는 문제는 해결되었지만, 마찬가지로 멀티스레드에 적합하지 않으며, 프로그램이 초기 로딩에 사용하는 자원이 많을 시에, lazy initialize을 할 수 없는 단점이 있다.

 

2-3. DCLP(Double-Checked Locking Pattern) 으로 구현

멀티스레드 환경에서, lazy initialize를 하면서 불필요한 Lock을 잡아 부하를 없앨 수 있는 방법이다.

public class Singleton{
	// Main memory 직접 접근을 위한 volatile 키워드
	private volatile static Singleton instance;
	
	private Singleton(){}
	
	public static Singleton getInstance(){
		if(instance == null){ // intance null check
			synchronized (Singleton.class){ 
				if(instance == null){ 
					instance = new Singleton();
				}				
			}			
		}
		return instance;
	}	
	
}

하지만 상기 DCLP로 구현한 코드 자체를 보았을 때는 멀티스레드에 안전해보이지만 적합하지 않다고 알려져있다.

잠깐 얘기하면, instance가 RAM에 있는게 아니라, CPU의 레지스터나 캐시에 있을 경우에 동기화, 일관성 문제(coherency problem)가 발생할 여지가 있다.

multi CPU, multicore 시스템의 경우는 더 가능성이 높아진다.

DCLP문제점에 대해서는 추후 포스팅하겠다.

 

2-4. holder에 의한 초기화 (Initialization on demand holder idiom)

Lazy initialization 방식을 가져가면서 Thread간 동기화 문제를 해결할 수 있다.

클래스안에 Holder를 두어 JVM의 class 로드되는 시점을 이용하여 static인 instance가 Holder안에 선언되도록하여서 한번만 호출되는 상황을 보장하도록 하는 방법인 것이다.

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

현재까지 제일 많이 사용되는 방법이라고 한다. 

 

2-5. Enum 초기화 (Enum initialization)

enum의 type은 역시 1번만 초기화되는 점을 사용한 방법이다.

public enum SingleTon {
 
        INSTANCE;
        public void do(String arg){
            //...
        }
}

해당 enum 으로 구현시 로딩시점에 static 으로 compile되어 초기화가 된다. 장점은 Eager initilization과 동일하다.

 

싱글톤패턴을 구현하는 방법은 어떤 방식이 무조건 좋다 라고는 할 수 없으며, 자기 프로그램에 맞는 적절한 방법을 사용하는 것이 중요한 것 같다.

반응형