-
Notifications
You must be signed in to change notification settings - Fork 0
Singleton Design Pattern
하나의 클래스에 하나의 객체만 생성 하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴
하나의 인스턴스만을 생성하는 책임이 있으며 getInstance 전역 메서드를 통해 모든 클라이언트에게 동일한 인스턴스를 반환하는 작업을 수행한다.
- 장점 :
- 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있음
- 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공한다.
- 매번 특정 객체를 생성할 필요 없이 하나만 생성된 객체를 어디에서든지 참조할 수 있다. 실생활에 적용하자면 프린터 하나를 여러명이서 사용할 경우를 예로 들 수 있다. 안드로이드 앱 같은 경우 각 액티비티나 클래스별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어 어디서나 접근하도록 설계하는 것이 편함
- 단점 :
-
싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 개방-폐쇄 원칙 을 위배하게 된다. (객체 지향 설계 원칙에 어긋남) 따라서 수정이 어려워지고 테스트하기 어려워진다.
-
multithreading 환경에서 Singleton 패턴을 사용한 클래스에 접근할 때, instance 가 1개 이상 생성되는 경우가 발생 (경합 조건) -> Lazy initialization (늦은 초기화 방식)을 사용할 경우 문제가 발생
결론 : 꼭 필요한 경우아니면 지양해야함. (적절히 잘 쓰면 아주 좋음)
- 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법 (Eager Initialization)
public class Printer {
// static 변수에 외부에 제공할 자기 자신의 인스턴스를 만들어 초기화
private static Printer printer = new Printer();
private Printer() { }
// 자기 자신의 인스턴스를 외부에 제공
public static Printer getPrinter(){
return printer;
}
public void print(String str) {
System.out.println(str);
}
}static 변수 : 객체가 생성되기 전 클래스가 메모리에 로딩될 때 만들어져 초기화가 한 번만 실행된다. : 프로그램 시작~종료까지 없어지지 않고 메모리에 계속 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있다.
장점 : static으로 생성된 변수에 싱글톤 객체를 선언했기 때문에 클래스 로더에 의해 클래스가 로딩 될 때 싱글톤 객체가 생성됩니다. 또 클래스 로더에 의해 클래스가 최초 로딩 될 때 객체가 생성됨으로 Thread-safe합니다.
단점 : 싱글톤객체 사용유무와 관계없이 클래스가 로딩되는 시점에 항상 싱글톤 객체가 생성되고, 메모리를 잡고있기 때문에 비효율적일 수 있다.
- 인스턴스를 만드는 메서드에 동기화하는 방법 (Thread-Safe Initialization)
public class Printer {
// 외부에 제공할 자기 자신의 인스턴스
private static Printer printer = null;
private int counter = 0;
private Printer() { }
// 인스턴스를 만드는 메서드 동기화 (임계 구역)
public synchronized static Printer getPrinter(){
if (printer == null) {
printer = new Printer(); // Printer 인스턴스 생성
}
return printer;
}
public void print(String str) {
// 오직 하나의 스레드만 접근을 허용함 (임계 구역)
// 성능을 위해 필요한 부분만을 임계 구역으로 설정한다.
synchronized(this) {
counter++;
System.out.println(str + counter);
}
}
}인스턴스를 만드는 메서드를 임계 구역으로 변경
-
다중 스레드 환경에서 동시에 여러 스레드가 getPrinter 메서드를 소유하는 객체에 접근하는 것을 방지한다. 공유 변수에 접근하는 부분을 임계 구역으로 변경
-
여러 개의 스레드가 하나뿐인 counter 변수 값에 동시에 접근해 갱신하는 것을 방지한다. getInstance()에 Lock을 하는 방식이라 속도가 느리다.
결론 :
- Initialization on demand holder idiom (holder에 의한 초기화)
이 방법은 클래스안에 클래스(Holder)를 두어 JVM의 Class Loader 매커니즘과 Class가 로드되는 시점을 이용한 방법입니다. Lazy initialization 방식을 가져가면서 Thread간 동기화문제를 동시에 해결할 수 있습니다.
중첩클래스 Holder는 getInstance 메서드가 호출되기 전에는 참조 되지 않으며, 최초로 getInstance() 메서드가 호출 될 때 클래스 로더에 의해 싱글톤 객체를 생성하여 리턴합니다. 우리가 알아둬야 할 것은 holder 안에 선언된 instance가 static이기 때문에 클래스 로딩 시점에 한번만 호출된다는 점을 이용한것이죠. 또 final을 써서 다시 값이 할당되지 않도록 합니다.
public class InitializationOnDemandHolderIdiom {
private InitializationOnDemandHolderIdiom(){}
private static class SingleTonHolder{
private static final InitializationOnDemandHolderIdiom instance = new InitializationOnDemandHolderIdiom();
}
public static InitializationOnDemandHolderIdiom getInstance(){
return SingleTonHolder.instance;
}
}