북극곰의 개발일기

7일차 싱글턴 패턴





posted by purplebeen on Sat Feb 03 2018 20:49:40 GMT+0900 (KST) in JAVA


싱글턴 패턴 : 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수있도록 하기 위한 패턴

ex ) 스레드 풀, 캐시, 대화상자, 캐시, 사용자설정, 레지스트리 설정 등

가장 기초적이고 오래된 싱글턴 패턴의 구현 방법은 public으로 생성된 클래스의 생성자를 private로 만들어 같은 클래스 외에는 접근을 하지 못하게 하고,

private static 형태의 정적 객체 변수를 만들어 그 객체 변수가 비어있을 경우 새로운 객체를 생성하도록 하는 방법이다. 이를 소스로 나타내면 다음과 같다.


package singleTonPattern;

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

자 그러면 이제 예제를 통해 조금 더 자세하게 살펴 보도록 하자.

초콜릿 공장에서 초콜릿을 끓이는 잧치인 ChocolateBoiler라는 객체가 있다.

이 객체는 아직 끓지 않은 500갤런의 초콜릿을 그냥 흘려 버린다거나 보일러가 가득 차 있는 상태에서 새로운 원료를 붓는다거나

하는 실수를 하지 않도록 새심하게 주의를 기울였다. 소스를 한번 보자.


package singleTonPattern;

public class ChocolateBoiler {
	private boolean empty;
	private boolean boiled;
	
	public ChocolateBoiler() {
		empty = true;
		boiled = false; 	//보일러가 비었을때
	}
	
	public void fill() {
		if(isEmpty()) {
			empty=false;
			boiled = false;
			//보일러에 우유 / 초콜릿 혼합한 재료를 집어 넣음
		}
	}
	
	public void drain() {
		if(!isEmpty() && isBoiled()) {
			//끓인 재료를 다음단계로 넘김
			empty = true;
		}
		
	}
	
	public void boil() {
		if(!isEmpty() && !isBoiled()) {
			//재료를 끓임
			boiled = true;
		}
	}
	public boolean isEmpty() {
		return empty;
	}
	
	public boolean isBoiled() {
		return boiled;
	}
}

자 그러면 이제 이 클래스에 싱글턴 패턴을 적용시켜보자.


package singleTonPattern;

public class ChocolateBoiler {
	private boolean empty;
	private boolean boiled;
	private static ChocolateBoiler uniqueChocolateBoiler;
	
	private ChocolateBoiler() {
		empty = true;
		boiled = false; 	//보일러가 비었을때
	}
	
	public static ChocolateBoiler getInstance() {
		if(uniqueChocolateBoiler == null) {
			uniqueChocolateBoiler = new ChocolateBoiler();
		}
		return uniqueChocolateBoiler;
	}
	
	public void fill() {
		if(isEmpty()) {
			empty=false;
			boiled = false;
			//보일러에 우유 / 초콜릿 혼합한 재료를 집어 넣음
		}
	}
	
	public void drain() {
		if(!isEmpty() && isBoiled()) {
			//끓인 재료를 다음단계로 넘김
			empty = true;
		}
		
	}
	
	public void boil() {
		if(!isEmpty() && !isBoiled()) {
			//재료를 끓임
			boiled = true;
		}
	}
	public boolean isEmpty() {
		return empty;
	}
	
	public boolean isBoiled() {
		return boiled;
	}
}

private static 형태의 ChocolateBoiler 객체를 생성하였고

이 객체의 값이 null일 경우에만 객체를 새로 생성하고 그렇지 않을 경우에는 그냥 객체를 반환하도록 getInstance()메소드를 설정했다.

이렇게 되면 단 하나의 객체만 생성되어 리턴될 것이다.

그런데 이 과정이 다중쓰레드(Multi Thread)를 사용할때도 그럴까?

멀티 쓰레드를 사용하는 과정에서 서로 다른 쓰레드가 getInstance()를 실행하다가 실행하는 순서와 시간의 차이 때문에

서로 같은 객체가 2번 생성될 우려가 있다. 이를 해결하기 위해서는 총 3가지의 방법이 있는데

  1. 동기화에 의한 속도 문제가 상관이 없다면, getInstance()메소드를 synchronized 형태로 바꿔 준다.
  2. 객체 인스턴스를 필요할 때 생성하는게 아니라 처음부터 선언해서 만들어버린다.
  3. jdk v1.4 이상일 경우, 메소드를 DCL(Double-Checking-Locking0을 사용하여 인스턴스가 생성되 있는지를 확인한 다음, 생성되어 있지 않을때만 동기화한다. 에를 들어 위에 우리가 짰던 간단한 SingleTon의 경우 이렇게 수정할 수 있다.

public class Singleton {
	private volatile static Singleton uniqueSingleton;

	private Singleton{};

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

이렇게 될 경우 인스턴스가 있는지 확인하고, 없으면 동기화 블럭으로 들어와 처음에만 동기화되고

블록으로 들어온 후에도 다시한변 변수가 null인지 확인한 후 객체를 생성한다.

volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Singleton 인스턴스를 초기하하는 과정이 올바르게 진행되도록 할 수 있다.