ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 싱글톤 패턴 (Singleton Pattern)
    Design Pattern/Creational Patterns 2020. 2. 4. 17:15

    싱글톤 패턴(Singmleton Pattern)은 해당 클래스의 객체가 프로그램 내에서 오직 한개의 인스턴스(Instance)를 생성하여 사용해야 할 때 사용합니다.

    가령 DB Connection 등과 같은 작업이 필요한 객체들이 각각 별도의 연결을 하게되면 많은 비용이 발생할 것입니다. 이럴 경우 DB Connection 작업을 하는 단일 객체를 통해 비용을 줄일 수 있습니다. 그리고 여러 모듈이나 자원을 관리하는 매니저(Manager) 객체를 싱글톤 패턴으로 생성하여 관리하는 경우에 사용할 수 있습니다.

     

    1. 고전적인 구현 방법

    싱글톤 패턴을 사용할 때 가장 기본적인 구현 방법입니다. 

    스스로의 객체를 필드로 가지며, 외부에서는 getInstance() 메소드를 통해 접근이 가능하도록 합니다.

    private 키워드와 함께 생성자(Constructor)를 만들어 외부에서 생성자에 접근할 수 없도록 합니다.

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

     

    2. synchronized 키워드를 활용하는 방법

    (1.) 고전적인 구현 방법의 문제점은 무엇일까요?

    getInstance() 메소드에서 싱글톤 객체의 생성 유무를 확인하는 조건문을 동시에 접근하는 경우 문제가 될 수 있습니다.

    객체가 생성되기 전 동시에 여러 스레드에서 조건문을 통과했다면, 싱글톤 패턴의 원칙을 깨고 여러 객체가 생성될 것입니다.

    이러한 문제를 해결하기 위해 처음 고안된 방법이 메소드 자체에 synchronized 키워드를 사용한 방법입니다. 메소드에 해당 키워드를 사용함으로써, 동시접근을 막고 안전하게 단일 객체를 생성할 수 있습니다.

    하지만, getInstance() 메소드 자체에 Lock이 걸리는 방식이기 때문에 성능 저하가 발생합니다.

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

     

    3. Double Checked Locking을 활용하는 방법

    (2.)에서 발생하는 성능 저하를 해결하기 위해 고안된 방법입니다. 

    객체가 이미 생성되어 있다면 그대로 단일 객체 instance를 반환합니다. 하지만, 객체가 생성되지 않은 시점일 경우 synchronized 키워드를 사용하여 잠그고, 다시 한번 생성 유무를 검사하고 객체를 생성하는 이중 체크(Double Check) 과정을 거치며 단일 객체를 사용합니다.

    하지만 이 방법에도 여전히 문제가 존재합니다. 

    소스코드를 컴파일(Compile)할 때 발생하는 재배치(Reordering) 때문입니다.

    인스턴스를 new 연산자를 통해 생성하도록 작성한 한 줄의 코드는 아래와 같이 이루어질 수 있도록 컴파일 될 것입니다.

     

      1. DoubleCheckedLockingSingleton 클래스의 객체를 위한 메모리 할당

      2. DoubleCheckedLockingSingleton 클래스의 생성자 실행

      3.할당된 메모리의 주소를 intance 변수에 대입

     

    하지만 컴파일러에서 상황에 따라 아래와 같이 (2.)와 (3.)의 순서를 바꿀 수 있습니다.

     

      1. DoubleCheckedLockingSingleton 클래스의 객체를 위한 메모리 할당

      2. 할당된 메모리의 주소를 intance 변수에 대입

      3. DoubleCheckedLockingSingleton 클래스의 생성자 실행

     

    문제는 재베치된 생성 과정의 (2.)에서 instance 변수에 아직 초기화 되지 않은 메모리가 대입되는 순간, 다른 스레드에서 getInstance() 메소드를 요청하게 되면서 발생합니다. 아직 new 연산자를 통해 실제 생성이 완료되지 않은 변수를 사용하게 되며 문제가 발생할 수 있습니다.

    public class DoubleCheckedLockingSingleton {
    	private static DoubleCheckedLockingSingleton instance;
    
    	private DoubleCheckedLockingSingleton() { }
    
    	public static DoubleCheckedLockingSingleton getInstance() {
    		if (instance == null) { 		//Single Checked
    			synchronized (instance) {
    				if(instance == null)	//Double Checked
    					instance = new DoubleCheckedLockingSingleton();
    			}
    		}
    		return instance;
    	}
    }

     

    4. 클래스 로딩 시점에 생성하는 방법

    instance 필드를 선언과 동시에 초기화하도록 하는 간단한 코드로 이전 방법들에서 발생하는 멀티 스레드 환경과 같은 동시 접근의 문제를 해결할 수 있습니다. 

    static 키워드와 함께 instance 필드가 선언되어 JVM에서 클래스가 로딩되는 시점에 new 연산자를 통한 초기화가 진행되기 때문에 단일 객체의 생성을 보장합니다.

     

    하지만 이러한 메커니즘에도 완벽하지 못하다고 하는데, 그 이유는 언제 사용할지도 모르는 객체를 미리 생성하기 때문에 객체의 생성에 많은 비용이 들어간다면 차지하는 메모리와 생성비용의 낭비이기 때문입니다.

    public class EarlySingleton {
    	private static EarlySingleton instance = new EarlySingleton();
    
    	private EarlySingleton() { }
    
    	public static EarlySingleton getInstance() {
    		return instance;
    	}
    }

     

    5. Enum을 활용하는 방법

    싱글톤 패턴을 Enum으로 구현함으로써 인스턴스가 여러개 생성되는 것을 방지할 수 있습니다.

    자바에서 Enum은 클래스처럼 필드와 메소드를 가질 수 있기 때문입니다.  

    public enum EnumSingleton {
    	INSTANCE;
    	
    	public static EnumSingleton getInstance() {
    		return INSTANCE;
    	}
    }

     

     

    6. LazyHolder 클래스를 사용한 방법

    객체를 필요한 시점에 초기화하며(Lazy Initialize) 단일 객체 생성의 성능을 보장하는 가장 완벽한 방법입니다.

     

    이전 방법들과 같이 instance 필드를 가지지 않고 싱글톤 클래스 내부에 LazyHolder 클래스의 필드를 선언하고 있지 않기 때문에 싱글톤(LazyHolderSingleton) 클래스가 로딩되는 시점에 LazyHolder 클래스를 로드하지 않고, 그 시점을 미루게 됩니다. 그리고 getInstance() 메소드 호출 시점에 (4.)와 동일하게 LazyHolder 클래스가 로딩되며 INSTANCE 필드가 선언과 동시에 초기화되며 이를 반환합니다.

    그리고 이러한 과정에서 마찬가지로 객체 생성의 책임을 클래스 로더(Class Loader)에게 넘기기 때문에 단일 객체 생성의 보장을 고민하지 않아도 되는 것입니다.

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

     

    댓글

Designed by Tistory.