* 본 포스팅은 한빛미디어의 헤드퍼스트 디자인 패턴을 공부한 내용을 정리한 글입니다.
저의 부족한 생각과 주관으로 틀린 내용이 있을 수 있으니,
자세한 내용이 궁금하시다면 해당 책을 읽어보시길 추천드립니다.
목차
0. 싱글턴이란?
1. 간단한 싱글턴
2. 스레드 안정성
2-1. 동기화(synchronized)
2-2. 인스턴스 미리 선언하기
2-3. Double-Checked Licking
2-4. lazyHolder(요청시 초기화 홀더) 패턴
3. 직렬화
4. 리플랙션
5. enum을 사용한 싱글턴
6. 싱글턴 남용의 위험성
0. 싱글턴(Singleton)이란?
스레드 풀, 또는 로그용 객체, 디바이스 드라이버등 하나만 있어도 충분하고,
여러개면 오히려 문제가 발생할 수 있는 공유객체 또는 유일하게 관리되어야할 객체를 관리하기 위한 패턴입니다.
1. 간단한 싱글턴
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton;
}
return singleton
}
}
위처럼 생성자를 외부에서 호출할 수 없도록 private 접근제한자를 설정합니다. 대신, 해당 클래스의 인스턴스는 getInstance()메소드를 통해 획득합니다. 위와같은방법을 lazy instantiation(게으른 인스턴스 생성)이라고 부릅니다.
위의 방법은 정말 단순한 예인데요, 여러모로 문제가 많습니다.
2. 스레드 안정성
스레드 1, 2가 Singleton 클래스의 getInstance()를 통해 instance를 참조하는 그림입니다.
위의 간단한 싱글턴으로 객체가 유일함을 보장하려고 했으나,
멀티 스레드 환경에서는 위의 그림처럼 스레드가 서로 다른 객체의 참조를 가질수도 있습니다.
그러므로, 한 번에 하나의 스레드만 접근할 수 있도록 동기화를 해야합니다.
2-1. 동기화(synchronized)
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton;
}
return singleton
}
}
synchronized 키워드를 사용하면 1나노초라도 늦게 호출한 스레드가 이전 스레드가 사용을 끝낼때까지 대기합니다.
해당 인스턴스에 접근하는 스레드가 많으면 많을수록, 스레드가 오래 사용할수록 속도에서 손해를 봅니다.
(100배정도의 성능저하가 있다는것이 일반적)
2-2. 인스턴스 미리 선언하기
public class Singleton{
private static Singleton singleton = new Singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
return singleton
}
}
클래스가 로딩될 떄, JVM에선 Singleton의 인스턴스를 생성하며,
new 키워드가 없기 떄문에 외부호출로 인스턴스를 생성할 수 없습니다.
2-3. Double-Checked Licking
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(singleton == null){
synchronized (Singlton.class){
if(singleton == null){
singleton = new Singleton;
}
}
}
return singleton
}
}
컴파일러가 최적화를 위해 연산의 순서를 변경할 수 있다는것 아시나요?
동기화 블록 내부에서 컴파일러로 인해 재배열이 일어날 수 있기에,
객체의 초기화 끝나지 않았는데, 객체에 대한 참조가 instance에 저장될수도 있습니다.
volatile는 instance 필드에 값을 쓸 때 이러한 재배열이 일어나지 않도록 컴파일러에게 지시해 위의 문제점을 방지해 줍니다.
하지만 volatile만으로는 원자성 보장을 할 수 없어 synchronized와 같이 사용해야 합니다.
그래서 결국 synchronized를 사용하기에 성능저하의 문제는 여전합니다.
그리고 volateile 키워드는 메인 메모리에 저장 변수를 저장하게끔 하는데, 이는 CPU Cache보다 메인 메모리의 비용이 더 크기 떄문에 사용에 신중해야 합니다.
2-4. lazyHolder(요청시 초기화 홀더) 패턴
public class Singleton {
private Singleton() {}
private static class LazyHolder() {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.instance;
}
}
getInstance()가 호출되면 LazyHolder의 instance변수에 접근하는데,
LazyHolder가 static class이므로 클래스의 초기화과정이 이루어 집니다.
그러면서 final로 선언한 instance의 객체생성도 진행되게 되는데, JVM이 해당 과정에서 원자성을 보장합니다.
이는 synchronized를 사용하지 않아도 JVM가 원자성을 보장해주는점을 이용해 Thread-Safe한 싱글톤 패턴을 구현할 수 있습니다.
3. 직렬화
클래스를 직렬화 할때 새 인스턴스가 생성되어 싱글턴 속성을 위반하게 됩니다.
해결법은 총 세가지가 있습니다.
- 싱글톤 오브젝트를 직렬화 하기 위해 implements Serializable을 추가한다.
- 모든 필드를 transient로 선언한다
- readResolve() 메소드를 구현한다.
private Object readResolve(){
return singleton;
}
4. 리플랙션
리플랙션을 이용하면, 런타임에 private 생성자에 접근해 새 인스턴스를 생성할 수 있습니다.
private 생성자였지만 Reflection을 이용해 public(accessible = true)으로 바꾸어 객체를 생성할 수 있죠.
void main(){
//singleton instance
Singleton singleton = Singleton.getInstance();
Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
//another singleton instance
Singleton singleton2 = (Singleton) constructor.newInstance();
}
5. enum을 사용한 싱글턴
Enum은 private 생성자로 인스턴스 생성을 제어하며, 상수만 갖는 특별한 클래스이기 때문에 싱글톤의 성질을 일반적으로 갖고 있습니다. 그리고 enum class의 생성자는 내부에서만 액세스 가능합니다.
즉, Enum Singleton 은 Thread-safety 와 Serialization이 보장되고,
Reflection을 통한 공격에도 안전하기 때문에 싱글톤을 구현하는 가장 좋은 방법입니다.
public enum Singleton{
singleton;
//기타 필드
}
//---- another class
main(){
Singleton singleton = Singleton.singleton
}
6. 싱글턴 남용의 위험성
유일성에 포커스를 맞추지 않고,
정적객체의 접근성의 편리성을 이용하기 위한무분별한 싱글턴 사용은 많은 문제를 야기합니다.
- mocking이 불가능해 테스트가 어려워진다.
- 동기화가 적절히 이루어지지 않는다면 경쟁상태를 야기한다.
- 싱글턴에 의존성을 가진 클래스가 변경에 취약해진다.
'개발공부 > Code design' 카테고리의 다른 글
헤드퍼스트 디자인 패턴 06.커맨드(Command) 패턴 (0) | 2022.12.10 |
---|---|
헤드퍼스트 디자인 패턴 04.팩토리(Factory) 패턴 - 팩토리 메서드 (0) | 2022.09.25 |
헤드퍼스트 디자인 패턴 04.팩토리(Factory) 패턴 - 심플 팩토리 (0) | 2022.09.25 |
헤드퍼스트 디자인 패턴 03.데코레이터(Decorator) 패턴 (0) | 2022.08.08 |
헤드퍼스트 디자인 패턴 2장. 옵저버(Observer)패턴 (0) | 2022.07.17 |