헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.
싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.
package singleton.before;
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
초콜릿 공장에서는 초콜릿을 끓이는 장치인 초콜릿 보일러를 컴퓨터로 제어한다.
이 보일러에서는 초콜릿과 우유를 받아서 끓이고 초코바를 만드는 단계로 넘겨준다. 여기에 초콜릿 보일러를 제어하기 위한 클래스가 나와 있다.
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
this.empty = true;
this.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 this.empty;
}
public boolean isBoiled() {
return this.boiled;
}
}
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
this.empty = true;
this.boiled = false;
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
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 this.empty;
}
public boolean isBoiled() {
return this.boiled;
}
}
싱글턴의 고전적인 구현법을 배웠다. 그렇다면 싱글턴 패턴의 정의는 무엇이고, 실제로 어떤 식으로 싱글턴 패턴을 적용해야할까??
💡 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823
고전적인 싱글턴을 이용해서 코드를 고쳤음에도 ChocolateBoiler에 있는 fill() 메소드에서 아직 초콜릿이 끓고 있는데 재료를 집어넣고 말았다. 무슨 일이 일어난 것일까??
다중 스레드
를 사용하도록 ChocolateBoiler 컨트롤러를 최적화시킨 것이 문제일까??두 개의 스레드에서 여기에 있는 코드를 실행시킨다고 가정해보고 두 스레드가 다른 보일러 객체를 사용하게 될 가능성이 있는지 따져보자.
ChocolateBoiler boiler = ChocolateBoiler.getInstacne();
boiler.fill();
boiler.boil();
boiler.drain();
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
문제를 해결하는 방법은 간단한데 바로 getInstance()를 동기화시키기만 하면 된다.
public static synchronized ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
대부분의 자바 애플리케이션에서 싱글턴이 다중 스레드 환경에서 돌아갈 수도 있도록 만들어야 한다. 하지만 getInstance() 메소드를 동기화시키려면 대가를 치뤄야 한다. 다른 방법은 없을까??
만약 getInstance() 메소드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔둬도 된다. getInstance()를 동기화시키는게 굉장히 쉽고, 효율 면에서도 나쁘지 않을 수있다.
하지만 메소드를 동기화하면 성능이 100배 정도 저하된다는 것을 기억하자. 만약 getInstance()가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해야 한다.
애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는 인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면 다음과 같은 식으로 처음부터 Singleton 인스턴스를 만들어버리는 것도 괜찮은 방법이다.
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
DCL을 사용하면 , 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있다. 이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않도록 동작하여, 바로 원하던 동작이 수행된다.
public class Singleton {
// 자바 5 이전 버전은 동기화 x
private volatile static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null) { // 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으러 진입
synchronized (Singleton.class) {
if(uniqueInstance == null) { // 블록으로 들어온 후레도 다시 한번 널체크한 후, 인스턴스를 생성한다.
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
Q) 모든 메서드와 변수가 static으로 선언된 클래스를 만들어도 결과적으로는 같지 않을까?
A) 맞다. 하지만 필요한 내용이 클래스에 다 들어있고, 복잡한 초기화가 필요없는 경우에만 해당 방법을 사용할 수 있다. 그리고 자바에서 정적 초기화를 처리하는 방법 때문에 복잡해질 수 있다. 특히 여러 클래스가 얽혀 있는 경우에는 지저분하고, 초기화 순서와 관련된 버그는 찾기 어렵기 때문에 해당 방식으로 싱글턴 비슷한 걸 만들어야 한다면 좋지 않을 수 있다.
Q) 클래스 로더와 관련된 문제는 없을까?
A) 클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문에 클래스 로더가 두 개 이상이라면 같은 클래스를 여러 번 로딩할 수도 있다. 만약 싱글턴을 그런 식으로 로딩하면 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있다. 따라서 클래스 로더를 여러 개 사용하면서 싱글턴을 사용한다면 조심해야 하고, 클래스 로더를 직접 지정해서 문제를 회피할 수도 있다.
Q) 전역 변수가 싱글턴보다 나쁜 이유는 무엇일까??
A) 자바의 전역 변수는 기본적으로 객체에 대한 정적 레퍼런스다. 전역 변수를 이런 식으로 사용한다면 게으른 인스턴스를 사용할 수 없는 단점과 싱글턴 패턴을 쓰는 두 가지 이유 중, 클래스의 인스턴스가 하나만 있을 수 있도록 할 수 없다. 전역 변수를 사용한다면 간단한 객체에 대한 전역 레퍼런스를 자꾸 만들게 도면서 네임스페이스를 지저분한게 만드는 경향이 생긴다.