반응형

싱글턴 패턴은 안티 패턴이라고 주장하는 사람도 많지만 여전히 많은 곳에서 사용되고 있다.

 

싱글턴이란 애플리케이션이 실행 중에 객체를 하나만 가질 수 있는 클래스를 의미한다.

 

싱글턴을 구현하는 방법은 무수히 많으며 이펙티브 자바에서는 세 가지를 설명한다.

 

public static final 변수로 접근 가능한 싱글턴

public class PublicStaticFinalSingleton {
	public static final PublicStaticFinalSingleton INSTANCE = new PublicStaticFinalSingleton();

	private PublicStaticFinalSingleton() {

	}

	public void hello() {
		System.out.println("hello");
	}
}


public class Rule3 {

	public static void main(String[] args) {
		PublicStaticFinalSingleton.INSTANCE.hello();

		new PublicStaticFinalSingleton(); // private 생성자 뿐이므로 컴파일 에러
	}
}

 

getInstance()와 같은 정적 팩토리 메서드로 접근 가능한 싱글턴

public class GetInstanceSingleton {
	private static final GetInstanceSingleton INSTANCE = new GetInstanceSingleton();

	private GetInstanceSingleton() {

	}

	public static GetInstanceSingleton getInstance() {
		return INSTANCE;
	}

	public void hello() {
		System.out.println("hello");
	}
}

public class Rule3 {

	public static void main(String[] args) {
		GetInstanceSingleton.getInstance().hello();

		new GetInstanceSingleton(); // private 생성자 뿐이므로 컴파일 에러
	}
}

 

위 두 방법은 거의 비슷한 방법으로 Eager initialization을 사용한 싱글톤 방식이라고도 한다. 하지만 몇 가지 문제점이 있다.

 

1. 클래스가 로드하게 되면 바로 객체를 생성한다. 만약 해당 싱글톤 클래스가 객체를 생성하는데 많은 시간과 비용이 발생한다면, 애플리케이션이 로드되는데 오래 걸리는 문제가 있을 수 있다. 

 

2. 리플렉션을 사용한다면 생성자가 private여도 호출할 수 있다. 생성자가 두 번 호출되면 예외를 던지도록 처리하여 방어할 수 있다.

 

3. 싱글턴 클래스가 Serializeable이 가능하다면 역직렬화 때마다 새로운 객체가 생성된다. 나중에 포스팅할 내용을 미리 포스팅하여 설명하자면 아래와 같다.

 

/**
 * 싱글턴 클래스는 오직 하나의 객체만 생성되게 하는 클래스이다.
 * 만약 Serializable을 구현한다면 readObject에 의해서 새로운 객체가 만들어지므로 싱글턴 클래스가 아니게 된다.
 *
 * @author gwon
 * @history
 *          2019. 6. 8. initial creation
 */
public class SingletonElvis implements Serializable {
	public static final SingletonElvis INSTANCE = new SingletonElvis();

	private SingletonElvis() {

	}

}

public class Rule77 {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		// 싱글턴으로 미리 구현된 동일한 객체만 사용 가능
		SingletonElvis elvis = SingletonElvis.INSTANCE;

		// 직렬화 후, 역직렬화
		SingletonElvis newElvis = (SingletonElvis) deserialize(serialize(elvis));

		System.out.println(elvis == newElvis); // false
	}
}

위와 같이 싱글톤을 역직렬 화하면 새로운 객체가 생성되어 객체가 2개 이상이 된다.

 

이를 방어하기 위해서는 아래와 같이 역직렬화 후 호출되는 메서드인 readResolve() 메서드를 항상 동일한 객체를 리턴하도록 작성하면 된다.

/**
 * 이를 막기 위해 readResolve 메서드를 구현하면 싱글턴 속성을 만족하게 오직 하나의 객체만 반환하게 하면 된다.
 *
 * @author gwon
 * @history
 *          2019. 6. 8. initial creation
 */
public class SingletonElvisWithReadResolve implements Serializable {
	public static final SingletonElvisWithReadResolve INSTANCE = new SingletonElvisWithReadResolve();

	private SingletonElvisWithReadResolve() {

	}

	// 역직렬화가 끝난 후, 해당 메서드가 호출되므로 항상 동일한 객체(싱글턴)을 반환하도록 함.
	private Object readResolve() {
		return INSTANCE;
	}

}

 

Enum을 사용한 싱글턴

위에서 언급한 리플렉션, 직렬화를 위한 방어 로직을 작성하기 귀찮다면 Enum을 사용하는 것도 좋은 방법이다.

public enum EnumSingleton {
	INSTANCE;

	public void hello() {
		System.out.println("hello");
	}
}

public class Rule3 {

	public static void main(String[] args) {
		EnumSingleton.INSTANCE.hello();
	}
}

Enum은 직렬화가 자동으로 처리되며, 리플렉션을 통한 공격에도 안전하다.

 

위까지가 이펙티바 자바에서 설명하는 싱글톤 내용이다.

 

Lazy Initialization, Lazy Holder

Eager initialization 싱글톤이 있다면 Lazy initialization 싱글톤도 있다. 소스는 아래와 같다.

public class LazyInitializationSingleton {
	private static LazyInitializationSingleton INSTANCE;

	private LazyInitializationSingleton() {}

	public static LazyInitializationSingleton getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new LazyInitializationSingleton();
		}
		return INSTANCE;
	}

	public void hello() {
		System.out.println("hello");
	}
}

Lazy Initialization은 INSTANCE 필드에 바로 초기화하지 않고 getInstance()를 최초로 호출될 때 객체를 생성하는 방식이다. 따라서 호출되지 않는다면 객체 생성이 미루어지므로 애플리케이션이 로드되는데 부담이 없다.

 

하지만 여러 쓰레드가 동시에 getInstance()를 호출할 경우 객체가 두 번 이상 호출될 수 있는 위험이 존재한다. 이를 방지하기 위해 getInstance()에 synchronized 키워드를 사용하여 해결할 수 있지만, 해당 싱글톤이 자주 사용된다면 효율적이지 않다.

 

따라서 이를 해결하는 방법이 Lazy Holder이다.

 

Lazy Holder는 JVM 클래스 로더의 동작 방식을 이용하여 Lazy Initialization를 만족하면서, 동기화 문제도 해결한다.

https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom

public class LazyHolderSingleton {
	private LazyHolderSingleton() {};

	public static LazyHolderSingleton getInstance() {
		return LazyHolder.INSTANCE;
	}

	private static class LazyHolder {
		private static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton();
	}

	public void hello() {
		System.out.println("hello");
	}
}

Lazy Holder의 원리는 다음과 같다.

 

1. LazyHoladerSingleton은 static 필드가 없기 때문에, 즉 초기화할 것이 없으므로 아주 빠르게 클래스가 로드된다.

2. LazyHolder는 LazyHolder가 실행되기 전까지는 로드가 되지 않는다.

3. 어디선가 LazyHoladerSingleton.getInstance()를 호출하면 LazyHolder를 로드를 수행하고 초기화를 진행한다.

4. LazyHolder가 로드되면서 static 필드인 INSTANCE를 초기화할 때는 JVM 원리상 동시에 수행할 수 없다.

5. 따라서 LazyHolder는 멀티스레드에서도 안전하게 LazyHolderSingleton의 생성자를 호출하여 초기화를 완료한다.

반응형

+ Recent posts