참고: Head First Design Patterns
프로그래밍을 하면서, 때로는 오직 단 하나만 존재해야 하는 객체가 필요할 수 있다. 예를 들자면 로깅(logging)을 위한 객체, 쓰레드 풀(thread pools) 등이 있다. 이런 객체들을 여러개를 인스턴스화 해버린다면 비연속적인 결과, 리소스의 낭비 등의 문제로 이어질 수 있다.
유일 객체의 필요성은 알겠다. 근데, 이를 구현하기 위해 디자인 패턴까지 필요한가?
static 키워드로는 안되는건가?public static MyClass myClass = new MyClass();
만약 전역 변수를 사용한다면, 해당 객체는 프로그램 시작과 동시에 생성될 것이다. 그런데, 만약 이 객체가 지속적으로 리소스를 잡아먹는 객체라면? 리소스의 낭비 문제가 생길 것이다.
반면, 싱글톤 패턴을 이용한다면 싱글톤 객체를 필요한 시점에 생성할 수 있으므로 리소스의 낭비를 줄일 수 있다.
싱글톤 패턴이 왜 필요한지 알아보았으니 어떻게 구현하는지, 어떻게 사용하는지를 코드를 통해 살펴보자.
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null)
uniqueInstance = new Singleton();
return uniqueInstance;
}
}
단순한 싱글톤 클래스의 구조이다. 구조 자체는 아주 단순한데, 생소한 코드들이 다소 보인다. 하나하나 살펴보면서 분석해보자.
private static Singleton uniqueInstance;
public static Singleton getInstance() {
if(uniqueInstance == null)
uniqueInstance = new Singleton();
return uniqueInstance;
}
이 클래스는 놀랍게도 자기 자신에 대한 static 변수를 가지고있다! 그러고는, getInstance() 메소드가 그 변수를 리턴해준다.
짐작 가능하겠지만, 이 변수가 우리의 유일 객체 역할을 할 것이다. 여기까지는 이해하기 크게 어렵지 않아보인다.
private이다!private Singleton() {}
생성자가 private하게 선언되었다는 것이 무슨 의미일까? 싱글톤 패턴을 처음 접해보았다면 생성자를 private하게 선언하는 경우를 처음 보았을 수도 있다. 당연하다. 생성자를 정의할 땐 보통 클래스 외부에서 이 클래스 객체를 생성하기 위함인데, private으로 선언했다는 것은 Singleton 클래스 내부 코드만이 Singleton을 생성할 수 있다는 뜻이 아닌가!
그런데 싱글톤 패턴의 목적을 잘 생각해보면 정확히 이 점이 싱글톤 클래스가 원하는 점이다.
Singleton은 Singleton 외부에서 인스턴스화되지 않는다. 이 점을 유의하고 getInstance() 메소드를 다시 보자.
if(uniqueInstance == null)
uniqueInstance = new Singleton();
return uniqueInstance;
만약 uniqueInstance가 null이라면, Singleton은 단 한번도 생성되지 않았다는 의미이다. 따라서, getInstance()의 최초 호출 시 메소드 내부에서 Singleton의 인스턴스를 비로소 생성하고 리턴한다. 이 때 우리의 private 생성자가 사용된다. 만약 null이 아니라면 이미 생성되었다는 의미이므로 그저 인스턴스를 리턴할 뿐이다.
싱글톤 패턴은 아래와 같이 정의한다.
어떤 클래스가 오직 하나의 인스턴스만을 가지며 전역 범위의 접근을 보장하는 디자인 패턴.
위 내용을 조금 더 분석해보자.
Singleton.getInstance().someUsefulMethod();
Singleton.getInstance()를 통해 호출할 수 있다.
이를 클래스 다이어그램으로 나타낸다면 위와 같이 단순한 구조이다.
초콜릿 공장의 초콜릿 보일러 기계를 관리하는 프로그램이 있다고 해보자.
public class ChocolateBoiler {
private static ChocolateBoiler uniqueInstance;
private boolean empty;
private boolean boiled;
private ChocolateBoiler() {
empty = true;
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 empty; }
public boolean isBoiled() { return boiled; }
}
ChocolateBoiler 클래스는 empty, boiled 두 가지 상태를 갖는다.
또한, fill(), drain(), boil()의 3가지 동작을 한다.
fill() : 초콜릿 보일러가 비어있다면, 보일러를 초콜릿과 우유로 채워주고 empty 상태를 false로 바꿔준다.drain() : 만약 비어있지 않고 가열되어 있다면, 보일러를 비워주고 empty를 다시 true로 바꿔준다.boil() : 만약 비어있지 않은데 가열되어있지 않다면 보일러를 가열하고 boiled를 true로 바꿔준다.우리의 초콜릿 보일러는 여러 객체가 생성되는 문제를 사전 차단하기 위해서 싱글톤 패턴을 잘 적용해서 구현하였다. 분명, 문제없이 작동하고 있었다. 그런데, 멀티 쓰레드(Multi Threading)를 사용하면서부터 문제가 터지기 시작했다.
수정한 내용이라고는, ChocolateBoiler를 이용하는 프로그램에 멀티 쓰레딩을 적용한 것 뿐이다. 각각의 쓰레드에서 getInstance()를 실행했을 뿐, 문제 될 건 없어 보인다.
서로 다른 두 쓰레드에서 동시에 Singleton.getInstance()가 실행된다면 어떤 결과를 초래할 수 있는지 실행 흐름과 함께 살펴보자.

두 쓰레드에서 각각 getInstance()가 호출되는 시점에서, uniqueInstance의 상태는 null이다. 따라서, 쓰레드1과 쓰레드2 모두 if(uniqueInstance == null) 조건문의 내용이 실행된다. 문제는 여기서 발생한다.
우리의 의도대로라면 쓰레드1에서 uniqueInstance = new ChocolateBoiler() 문장이 실행되면 실행되면, uniqueInstance가 이미 생성되었으므로 쓰레드2의 조건문을 통과하지 못해야 하지만, 통과해버린 것이다.
uniqueInstance를 두 번 생성해 버렸다.이 문제를 해결 할 수 있는 방법은 어떤 것이 있을까?
getInstance() 메소드를 synchronized로 바꾸면 되지 않을까?
public static synchronized ChocolateBoiler getInstance() { ... }
이렇게 하면 분명 싱글톤 객체의 중복 생성 문제는 막을 수 있다. 원리는 단순하다. synchronized 키워드를 추가함으로써 getInstance() 메소드가 여러 쓰레드에서 동시에 호출된다면 차례를 기다리도록 해서 메소드의 동시 실행을 막는 것이다.
getInstance()의 최초 실행 이후에도 과연 동기화가 필요할까? 첫 싱글톤 인스턴스가 생성된 시점부터는 더 이상 uniqueInstance는 null이 아닐 텐데도 다른 쓰레드의 getInstance()가 완료될 때 까지 대기하도록 하는 것은 효율적이지 못하다!
즉, 매번 synchronized되는 getInstance()가 성능에 치명적이지 않다면 이는 분명 해결책이지만, 성능이 중요한 경우라면 다른 방법을 찾는 것이 옳다.
두 번째 해결 방법은 의외로 간단하다. 싱글톤 패턴의 인스턴스 생성 시점에 있어서의 장점을 포기하면 된다.
private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();
public static ChocolateBoiler getInstance() {
return uniqueInstance;
}
이렇게 한다면 싱글톤 클래스를 호출하는 순간 JVM(Java Virtual Machine)이 인스턴스를 생성해줌으로써, 쓰레드가 인스턴스의 생성 이후에 싱글톤에 접근함을 보장해준다.
다만, 런타임에 인스턴스를 생성할 수 있다는 점이 기존 코드의 장점이었으므로, 역시 이러한 변경이 프로그램의 성능에 얼마나 영향을 주는지를 잘 따져보고 적용해야 한다.
우선 코드를 먼저 살펴보자.
private volatile static ChocolateBoiler uniqueInstance;
public static ChocolateBoiler getInstance() {
if(uniqueInstance == null) {
synchronized(uniqueInstance.class) {
if(uniqueInstance == null)
uniqueInstance = new ChocolateBoiler();
}
}
}
달라진 부분은 volatile 키워드와 synchronized 부분이다.
volatile 키워드는 간단히 설명하자면, 변수의 값을 CPU 캐시가 아닌 메인 메모리(Main Memory)에 저장하겠다는 의미이다. 이를 통해 여러 쓰레드들이 uniqueInstance에 잘 접근하도록 보장해준다.synchronized를 사용하는 것은 방법1과 동일하지만, 조건문 안에서 실행한다. 이것이 갖는 의미는 오직 메소드의 첫 호출시에만 동기화가 실행된다! 따라서, getInstance()의 성능이 문제라면 위 방법을 사용하는 것이 좋다.
지금까지 싱글톤 패턴의 목적과 사용 방법을 알아보았다. 여전히 왜 전역 변수 대신 싱글톤 패턴을 사용하는 것이 좋은지 의문이라면, 싱글톤 패턴의 정의를 통해 정리할 수 있다.
어떤 클래스가 (1) 오직 하나의 인스턴스만을 가지며, (2) 전역 범위의 접근을 보장하는 디자인 패턴.
전역 변수는 (1) 번은 확실히 보장하지만, (2) 번을 보장해주지 못한다. 즉, 개발자가 static 키워드를 코드 어딘가에서 남발해서 똑같은 클래스의 여러 static 변수들을 생성했을 수도 있다는 것이다. 반면, 싱글톤 객체는 public 생성자를 갖지 않으므로 외부에서 인스턴스화 하는 것을 막아준다.
다만, 위에서 살펴본 멀티 쓰레딩 예시 외에도 복수의 클래스 로더 사용 시 인스턴스가 중복 생성되는 문제가 발생할 수 있으므로 사용에 유의하여야 한다.
잘 보고 갑니다~~