싱글턴(Singleton pattern) 이란?
싱글턴(Singleton) 패턴은 하나의 객체만을 생성할 수 있는 클래스를 말합니다.
이를 사용해 객체의 유일성을 보장할 수 있습니다.
그렇다면 객체의 유일성을 어떻게 보장할 수 있을까요?
아래의 코드 예제를 보도록 하겠습니다.
public class Singleton {
public static final Singleton instance = new Singleton();
}
위의 코드 예제를 보면 정적(static) 변수를 사용한 것을 볼 수 있습니다.
정적 변수는 독립적인 저장 공간을 가지는 인스턴스와 다르게 공통된 저장 공간을 가집니다.
즉, 한 클래스로부터 생성되는 모든 인스턴스들이 값을 공유하게 됩니다.
이는 이전 글인 클래스와 메서드에 자세히 설명되어 있습니다.
[JAVA 이론] 필드(Field)와 메서드(method)
[JAVA 기초] 1. 클래스와 객체 2. 필드(Field)와 메서드(method) 3. 생성자(Constructor) 4. 내부 클래스(Inner Class) 개요 클래스는 크게 네 가지 요소로 구성되어 있습니다. 각각 필드(field), 메서드(method), 생성
developingman.tistory.com
또한 final을 선언하여 이 필드에 다른 객체가 할당되거나, 이미 할당된 객체를 싱글턴 내부에서 다시 객체를 할당하는 실수를 방지합니다.
그리고 외부에서 생성자를 통해 객체를 생성할 수 없도록 생성자의 접근 제어자를 private로 설정합니다.
public class Singleton{
public static final Singleton instance = new Singleton();
private Singleton(){}
...
}
// or
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance
}
...
}
아래의 예제 코드는 정적 필드로 접근가능한 모습을 볼 수 있습니다.
// 생성자의 접근 제어자가 private이기에 생성자로 객체를 생성할 수 없다.
// Singleton singleton = new Singleton(); (X)
// instance는 final로 선언되었기 때문에 외부에서 다시 지정하는 것은 불가능하다.
// Singleton.instance = null; (X)
// 외부에서 정적 필드로 다음과 같이 접근할 수 있따.
Singleton.Instance.service();
// or
Singleton singleton = Singleton.getInstance();
singleton.service();
싱글턴의 장점과 단점
싱글턴의 장점은 아래와 같습니다
- 싱글턴으로 구현한 인스턴스는 클래스 인스턴스로써, 다른 인스턴스와 데이터를 공유하기 때문에 메모리 낭비를 방지할 수 있습니다.
- 인스턴스가 단 하나이기 때문에 인스턴스를 생선 할 때 드는 비용이 줄어듭니다.
- 싱글턴으로 구현한 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유할 수 있습니다.
- 여러 객체의 설정값을 공유할 때 사용하면 여러 스레드가 동시에 해당 인스턴스를 공유하게 할 수 있습니다.
싱글턴 패턴의 단점은 아래와 같습니다.
- 객체 지향 설계 원칙 중 개방, 폐쇄 원칙이란 것이 존재합니다. 만약 싱글턴 인스턴스가 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데, 이때 개방, 폐쇄 원칙에 위배됩니다.
- 결합도가 높아지면 수정작업이나 테스트를 진행하기 어려워집니다.
- 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 1개 이상 생성되는 문제가 발생할 수 있습니다.
멀티스레드 환경에서 안전한 싱글턴 만드는 방법
1. Eager initialization (이른 초기화 방식)
싱글턴의 가장 기본적인 Eager initialization 방식입니다. 먼저 클래스 내에 전역 변수로 인스턴스 변수를 생성하고 static 키워드를 통해 인스턴스와 상관없이 접근이 가능하면서 동시에 접근 제어 키워드를 붙여서 initialization.instance로 접근할 수 없도록 합니다.
생성자에서도 접근 제어 키워드를 붙여 다른 클래스에서 새로운 인스턴스를 생성하는 것을 방지합니다.
오로지 getInstance() 메서드를 이용해서 인스턴스에 접근하도록 하여 유일 무이한 동일 인스턴스를 사용하는 기본 싱글턴 원칙을 지키도록 합니다.
이른 초기화 방식은 싱글턴 객체를 미리 생성해 놓는 방식입니다. 항상 싱글턴 객체가 필요하거나 객체 생성비용이 크게 들어가지 않는 경우에 사용합니다.
package SingleTonExample;
public class EagerInitialization {
//private static으로 선언
private static EagerInitialization instance = new EagerInitialization();
//생성자
private EagerInitialization() {}
//인스턴스 리턴
public static EagerInitialization getInstance() {
return instance;
}
}
장점 : static으로 생성된 변수에 싱글턴 객체를 선언하였기에 클래스 로더에 의해 클래스가 로딩될 때 싱글턴 객체가 생성됩니다. 또 클래스 로더에 의해 클래스가 최초 로딩 될 때 객체가 생성됨으로 Thread-safe 합니다.
단점 : 싱글턴객체 사용유무와 관계없이 클래스가 로딩되는 시점에 싱글턴 객체가 생성되니 메모리 비용이 소모됩니다.
2. Lazy initialization (늦은 초기화 방식)
Eager initialization과는 정반대로 클래스가 로딩되는 시점이 아닌 클래스의 인스턴스가 사용되는 시점에서 싱글턴 인스턴스를 생성합니다. 그로 인해 사용 전까지 메모리를 점유하지 않습니다.
package SingleTonExample;
public class LazyInitialization {
private static LazyInitialization instance;
private LazyInitialization(){}
public static LazyInitialization getInstance(){
if(instance == null){
instance = new LazyInitialization();
}
return instance;
}
}
장점 : 싱글턴 객체가 필요할 때 인스턴스를 얻을 수 있습니다. Eager initialization 방식에 단점을 보완할 수 있습니다.
단점 : 멀티스레드 환경에서 동시에 getInstance를 호출할 때 인스턴스가 두 번 생성될 여지가 있습니다.
3.Thread safe Lazy initialization(스레드 안전한 늦은 초기화)
Lazy initialization 방식에서 thread-safe 하지 않다는 단점을 보완하기 위해 멀티 스레드에서 스레드들이 동시에 접근하는 동시성을 synchronized(동기화) 키워드를 이용해 해결합니다.
package SingleTonExample;
public class ThreadSafeLazyInitialization{
private static ThreadSafeLazyInitializationinstance;
private ThreadSafeLazyInitialization(){}
public static synchronized ThreadSafeLazyInitializationgetInstance(){
if(instance == null){
instance = new ThreadSafeLazyInitialization();
}
return instance;
}
}
장점: Lazy initialzation의 단점을 보완한다.
단점: synchronized는 큰 성능저하를 발생시키므로 권장하지는 않는 방법
3-1. Thread safe Lazy initialization + Double-checked locking 기법
Thread safe Lazy initialization의 성능 저하를 완화시키는 방법입니다.
package SingleTonExample;
public class ThreadSafeLazyInitialization {
private static ThreadSafeLazyInitialization instance;
private ThreadSafeLazyInitialization(){}
public static ThreadSafeLazyInitialization getInstance(){
//Double-checked locking
if(instance == null){
synchronized (ThreadSafeLazyInitialization.class) {
if(instance == null)
instance = new ThreadSafeLazyInitialization();
}
}
return instance;
}
}
첫 if문에서 인스턴스가 null인 경우 synchronized 블록에 접근하고 한번 더 if문을 통해 인스턴스의 null 유무를 체크합니다.
인스턴스가 null인 경우에 new 키워드를 통해 인스턴스화하고, 인스턴스가 null이 아니면 synchronized 블록을 수행하지 않습니다. 이런 Double-checked locking 기법을 통해 성능 저하를 보완할 수 있습니다.
4. Initialization on demand holder idiom (holder에 의한 초기화)
클래스 안에 Holder를 두어 JVM의 Class Loader 메커니즘과 Class가 로드되는 시점을 이용하는 방법입니다.
Lazy initialization 방식을 가져가면서 Thread 간 동기화 문제를 동시에 해결할 수 있습니다.
중첩클래스 Holder는 getInstance 메서드가 호출되기 전에는 참조되지 않으며, 최초로 getInstance() 메서드가 호출될 때 클래스 로더에 의해 싱글턴 객체를 생성하여 리턴합니다.
또한, holder 안에 선언된 instance가 static이기에 클래스 로딩 시점에 한 번만 호출된다는 점을 이용하며, final을 써서 다시 할당되지 않게 하는 점을 이해하여야 합니다.
package SingleTonExample;
public class InitializationOnDemandHolderIdiom {
private InitializationOnDemandHolderIdiom(){}
private static class SingleTonHolder{
private static final InitializationOnDemandHolderIdiom instance = new InitializationOnDemandHolderIdiom();
}
public static InitializationOnDemandHolderIdiom getInstance(){
return SingleTonHolder.instance;
}
}
reference
[Design_Pattern] Singleton(싱글톤)의 고도화
'JAVA' 카테고리의 다른 글
[JAVA] 람다 (Lambda) (0) | 2023.03.16 |
---|---|
[JAVA] 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy) (0) | 2023.03.15 |
[JAVA] 한글 깨짐 & error unmappable character (0xEB) for encoding x-windows-949 에러 해결 (0) | 2023.03.13 |
[JAVA] 어노테이션 (Annotation) (0) | 2023.03.12 |
[JAVA] 컬렉션 프레임워크(Collection Framework) - Iterator (6) (0) | 2023.03.11 |