헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 5. 하나뿐인 특별한 객체 만들기 - 싱글턴 패턴
싱글턴 패턴의 용도
인스턴스를 하나만 만들어야 하는 객체
- 스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체와 같이 하나만 있을 때 잘 동작하는 객체들이 있다.
- 이러한 객체들은 인스턴스가 2개 이상이면 프로그램이 이상하게 돌아간다거나, 결과에 일관성이 없어지는 등 문제가 발생할 수 있다.
전역 변수로 사용하거나 정적 변수를 사용하는 방법은?
- 싱글턴 패턴은 특정 클래스에 객체 인스턴스가 하나만 만들어지도록 해주는 패턴
- 싱글턴 패턴을 사용하면 전역 변수를 사용할 때와 마찬가지로 객체 인스턴스를 어디서든 액세스할 수 있게 만들 수 있으며, 전역 변수의 단점을 감수할 필요가 없어진다.
- 전역 변수가 자원을 많이 잡아먹는 객체라면 애플리케이션이 종료될 때까지 자원을 차지하고 있게 되는 점
고전적인 싱글턴 패턴 구현방법
public class Singleton {
private static Singleton uniqueInstance;
// 기타 인스턴스 변수
// 생성자를 private로 선언했으므로 Singleton 에서만 클래스의 인스턴스를 만들 수 있다.
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
싱글턴 패턴의 정의
- 싱글턴 패턴은 클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공한다.
- 싱글턴 객체의 인스턴스가 필요하다면 반드시 클래스 자신을 거치도록 해야 한다.
- 어디서든 인스턴스에 접근할 수 있도록 전역 접근 지점을 제공한다.
- 자원을 많이 잡아먹는 인스턴스가 있다면 싱글턴 패턴 기법이 유용하다.
| Singleton | | --------------------- | | static uniqueInstance | | // 기타 데이터 | | --------------------- | | static getInstance() | | // 기타 메서드 |
고전적인 싱글턴 패턴의 초콜릿 보일러
- 초콜릿 보일러는 초콜릿과 우유를 받아서 끓이고 초콜릿을 만드는 단계로 넘겨준다.
- 아직 끓지 않은 재료를 넘겨준다던가, 보일러가 가득 찬 상태에서 새로운 재료를 붓는다거나, 빈 보일러에 불을 올리는 실수를 하지 않도록 주의하여 작성했다.
public class ChocolateBoiler { private boolean empty; private boolean boiled; private 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; } }
멀티스레딩 문제
- 2개의 스레드에서 아래 코드를 실행해본다고 가정해보자.
ChocolateBoiler boiler = ChocolateBoiler.getInstance(); boiler.fill(); boiler.boil(); boiler.drain();
- 두 스레드가 다른 보일러 객체를 사용하게 될 가능성은 없는지 따져본다.
멀티스레딩 문제 해결해보기 - synchronized
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
// synchronized 키워드를 추가하면 한 스레드가 메서드 사용을 끝내기 전까지 다른 스레드는 기다린다. (동기화)
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
- 동기화가 꼭 필요한 시점은
getInstance()
메서드가 시작되는 때 뿐- 동기화를 하게 되면 속도 이슈가 발생할 수 있다!
더 효율적으로 멀티스레딩 문제 해결하기
getInstance()
의 속도가 그리 중요하지 않다면 동기화로 해결한다.- 인스턴스가 필요할 때는 생성하지 말고 처음부터 만든다.
- 싱글턴 인스턴스를 생성하고 계속 사용하거나, 인스턴스 실행 중에 수시로 만들고 관리하기가 성가시다면 처음부터 인스턴스를 만든다. ```java public class Singleton { // 정적 초기화 부분에서 싱글턴 인스턴스를 생성한다. 이러면 스레드를 써도 문제 없을 수 있다. private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() { // 인스턴스는 이미 초기화되었으니 반환만 해준다. return uniqueInstance; } } ```
이 방법을 사용하면 클래스가 로딩될 때 JVM에서 싱글턴의 하나뿐인 인스턴스를 생성해준다. JVM에서 하나뿐인 인스턴스를 생성하기 전까지 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.
DCL
을 써서getInstance()
에서 동기화되는 부분을 줄인다.DCL (Double-Checked Locking)
을 사용하면 인스턴스가 생성되어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화할 수 있다.- 이러면 처음에만 동기화하고 나중에는 동기화하지 않아도 된다.
DCL
은 자바 1.4 이전 버전에서는 사용할 수 없다. ```java public class Singleton { // volatile을 사용하면 멀티스레딩을 사용하더라도 uniqueInstance 변수가 Singleton 인스턴스로 초기화가 잘 된다. private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() { if (uniqueInstance == null) { // 인스턴스가 없으면 동기화 블록 진행 // 처음에만 동기화하도록 구현 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } } } ```
초콜릿 보일러 다시 작성해보기
DCL
방식을 사용하여 초콜릿 보일러를 다시 작성해본다.public class CholocateBoiler { private volatile static CholocateBoiler uniqueBoiler; private boolean empty; private boolean boiled; private CholocateBoiler() { empty = true; boiled = false; } public static CholocateBoiler getInstance() { if (uniqueBoiler == null) { synchronized (CholocateBoiler.class) { if (uniqueBoiler == null) { uniqueBoiler = new CholocateBoiler(); } } } } public void fill() { if (uniqueBoiler.isEmpty()) { uniqueBoiler.empty = false; uniqueBoiler.boiled = false; // 재료를 넣는 로직 } } public void drain() { if (!uniqueBoiler.isEmpty() && uniqueBoiler.isBoiled()) { // 끓인 재료를 다음 단계로 넘김 uniqueBoiler.empty = true; } } public void boil() { if (!uniqueBoiler.isEmpty() && !uniqueBoiler.isBoiled()) { // 재료를 끓임 uniqueBoiler.boiled = true; } } public boolean isEmpty() { return uniqueBoiler.empty; } public boolean isBoiled() { return uniqueBoiler.boiled; } }
Enum으로 싱글턴 구현하기
- Enum으로 싱글턴을 생성하면 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제를 해결할 수 있다. ```java public enum Singleton { UNIQUE_INSTANCE; // 기타 필요한 필드 }
public class SingletonClient { public static void main(String args[]) { Singleton singleton = Singleton.UNIQUE_INSTANCE; // 싱글턴 사용 } } ```
- 앞에서
getInstance()
메서드를 구현했던 부분은 싱글턴 작동 원리를 알아보기 위해 따라가본 것으로 생각하자.- 싱글턴이 필요할 때면 Enum을 사용해 구현하자.