싱글톤 패턴

싱글톤 패턴은 애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴을 말한다.

생성자가 여러번 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 반환한다.[자바에서는 생성자를 private으로 선언해서 생성 불가능하게 하고 getInstance()로 받아서 사용한다.]

따라서, 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴이라고 말할 수 있다.
(인스턴스가 필요할 때 똑같은 인스턴스를 만들어 내는 것이 아니라, 동일(기존) 인스턴스를 사용하게 한다.)

싱글톤 패턴을 쓰는 이유

고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있다. 또한 싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.

DBCP(DataBase Connection Pool)처럼 공통된 객체를 여러 개 생성해서 사용해야 하는 상황에서 많이 사용한다.
(쓰레드풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정, 로그 기록 객체 등)

안드로이드 앱 같은 경우 각 액티비티나 클래스별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어 어디서나 접근하도록 설계하는 것이 편하다.

  • 인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶은 경우 사용.
  • 두 번째 이용시부터는 객체 로딩 시간이 현저하게 줄어 성능이 좋아지는 장점.

싱글톤 패턴의 문제점

싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 개방-폐쇄 원칙을 위배하게 된다.(= 객체지향 설계 원칙에 어긋난다.)
[객체 지향 설계 원칙 찾아보기]

따라서 수정이 어려워지고 테스트하기도 어려워진다. 또한 멀티 쓰레드 환경에서 동기화 처리를 안하면 인스턴스가 두 개가 생성된다던지 하는 경우가 발생할 수 있다. 즉, 싱글톤이 Thred-safe 하지않을 수 있다. 불필요한 오버헤드가 생길 수 있다.

멀티쓰레드에서 안전한(Thred-safe) 싱글톤 클래스, 인스턴스 만드는 방법

1. Thread safe Lazy initialization(게으른 초기화)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingleTon{

private static SingleTon instance;

private SingleTon(){
// private한 생성자
}

public static synchronized SingleTon getInstance(){
if(instance == null){
instance = new SingleTon();
}
return instance;
}

}

위의 방식은 private static으로 인스턴스 변수를 만들고 private 생성자로 외부에서 생성할 수 있는 여지를 막았으며 synchronized 키워드를 사용해서 thread-safe하게 만들었다.

하지만 synchronized 특성상 비교적 큰 성능 저하가 발생하므로 권장하는 방법은 아니다. synchronized 키워드는 100배 정도 성능 저하를 가져온다고 한다.

  • 때문에 싱글톤 패턴으로 만들어진 클래스의 인스턴스에 volatile 키워드를 사용하여 멀티 쓰레드의 가시성 문제를 해결하여 모든 객체에 대해 synchronized 키워드가 불리는 것을 막아 성능을 향상시킨다.

2. Thread safe lazy initialization + Double-checked locking

게으른 초기화의 성능 저하를 완화시키는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SingleTon {

private volatile static SingleTon instance;

private SingleTon(){}

public static SingleTon getInstance(){

if(instance == null){
synchronized (SingleTon.class) {
if(instance == null)
instance = new SingleTon();
}

}
return instance;
}
}

getInstance()에 synchronized를 사용하는 것이 아니라 첫 번째 if문으로 인스턴스의 존재 여부를 체크하고 두 번째 if문에서 다시 한 번 체크할 때 동기화시켜서 인스턴스를 생성하므로 thread-safe하면서도 처음 생성 이후에 synchronized 블록을 타지 않기 때문에 성능 저하를 완화했다. 이렇게 volatile 키워드를 사용하는 것과 두 번 체크하는 기법을 double-checked locking이라고 한다.

그러나 완벽한 방법은 아니다. 이 방법은 메소드에서 synchronized를 빼면서 동기화 오버헤드를 줄여보고자 하는 의도로 설계된 방법이다. instance가 null인지 체크하고 null일 경우에만 동기화 블록에 진입하게 된다. 그래서 최초 객체가 생성된 이후로는 동기화 블록에 진입하지 않기 때문에 효율적인 방법이라고 생각할 수 있다.

하지만 아주 안좋은 케이스로 정상 동작하지 않을 수 있다. 예를 들어, Thread A와 Thread B가 있다고 하자. Thread A가 instance의 생성을 완료하기 전에 메모리 공간에 할당이 가능하기 때문에(static이기 때문에 Class가 로딩될 때 메모리에 할당됨) Thread B가 할당된 것을 보고 instance를 사용하려고 하나 생성과정이 모두 끝난 상태가 아니기 때문에 오동작할 수 있다는 것이다. 물론 이런 확률은 적겠지만 혹시 모를 문제를 생각하여 사용하지 않는 것이 좋다.

3. Initialization on demand holder idiom (holder에 의한 초기화)

클래스안에 클래스(Holder)를 두어 JVM의 Class loader 매커니즘과 Class가 로드되는 시점을 이용한 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Something {
private Something() {
}

private static class LazyHolder {
public static final Something INSTANCE = new Something();
}

public static Something getInstance() {
return LazyHolder.INSTANCE;
}
}

개발자가 직접 동기화 문제에 대해 코드를 작성하고 문제를 회피하려 한다면 프로그램 구조가 그만큼 복잡해지고 비용 문제가 생길 수 있고 특히 정확하지 못한 경우가 많다.(100%가 아닐 수 있다…ㅎ)

그런데 위의 3번 방법은 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용하여 싱글톤의 초기화 문제에 대한 책임을 JVM에게 떠넘긴다.

holder 안에 선언된 INSTANCE가 static이기 때문에 클래스 로딩 시점에 한번만 호출될 것이며 final 키워드를 사용해 다시 값이 할당되지 않도록 만든 방법이다.

로직을 설명하면 다음과 같다.

위의 Something 클래스에는 LazyHolder 클래스의 변수가 없기 대문에 Something 클래스 로딩시 LazyHolder 클래스를 초기화하지 않는다.

LazyHolder 클래스는 Something 클래스의 getInstance() 메소드에서 LazyHolder.INSTANCE를 참조하는 순간 Class가 로딩되며 초기화가 진행된다. Class를 로딩하고 초기화하는 시점은 thread-safe를 보장하기 때문에 volatile이나 synchronized 같은 키워드가 없어도 thread-safe 하면서 성능도 보장하는 아주 훌륭한 방법이라고 할 수 있다.

가장 많이 사용하고 일반적인 방법이다.