싱글턴 패턴(Singleton Pattern)

Head First - Design Patterns
(에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠 저 | 서환수 역)
을 읽고 정리한 내용입니다.

싱글턴 패턴(Singleton Pattern)

  • 싱글턴 패턴은 요즘 거의 안티패턴으로 여겨지고 있는 패턴임을 염두에 두고 공부해보자.
  • 안티패턴으로 여겨지는 이유 중 하나는 생성자가 private으로 선언되어 있어
    서브 클래스를 만들 수 없다는 점이다.

싱글턴 패턴의 정의

  • 특정 클래스에 대해서 객체 인스턴스가 하나만 만들어질 수 있도록 해주는 패턴
  • 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴

싱글턴 패턴을 적용하는 방법

  • 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만든다.
  • 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 구현한다.
  • 다른 객체에서 해당 인스턴스가 필요하면 언제든지 클래스한테 요청을 할 수 있고,
    요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만든다.

고전적인 싱글턴 패턴 구현법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
// 싱글턴의 유일한 인스턴스가 저장되는 정적변수
private static Singleton uniqueInstance;
// 생성자를 private으로 선언함으로써, Singleton에서만 클래스의 인스턴스 생성 가능
private Singleton(){}
/*
getInstance()
: 클래스의 인스턴스를 만들어서 리턴해주는 메서드
getInstance()를 사용하면 언제 어디서든 이 메서드 호출 가능
-> 전역 변수에 접근하는 것만큼이나 쉽다.
게으른 인스턴스 생성을 활용할 수 있다.
*/
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
  • 일반적으로 Singleton 객체를 얻는 static 메서드명은 getInstance()로 이름 붙인다.
  • 여기에서 getInstance()는 일반 메서드명이므로,
    반드시 이 메서드명으로 만들어야 하는 것은 아니다.
  • getInstance()이 아닌 get()이나 다른 메서드명으로 만들어도 된다.

게으른 인스턴스 생성(lazy instantiation)

  • 위의 코드에서 getInstance() 메서드 부분을 좀 더 자세히 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Singleton getInstance() {
// uniqueInstance가 null이라면,
// 아직 인스턴스가 생성되지 않았다는 것을 의미한다.
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
/*
인스턴스 생성 전이라면,
private으로 선언된 생성자를 이용해서 Singleton 객체를 만든다.
uniqueInstance에 그 생성된 객체를 대입한다.
이렇게 할 경우, 인스턴스가 필요하기 전까지는 아예 인스턴스를 생성하지 않는다.
이 방법을 게으른 인스턴스 생성이라고 한다.
*/
}
// uniqueInstance가 null이 아니라면,
// 인스턴스가 존재하고 있다는 의미 -> 그 인스턴스를 리턴하고 끝난다.
return uniqueInstance;
}
  • 싱글턴이 게으르게 생성되도록 구현할 수도 있는데,
    여기에서 게으르게(lazy)라는 표현은 필요할 때 만든다는 것을 의미한다.

멀티 스레드 환경에서의 문제점

  • 두 개 이상의 스레드에서 위의 Singleton.getInstance() 메소드를 실행시킨다고 가정해보자.
  • 두 개 이상의 스레드가 getInstance() 메서드에 진입하여 제어권이 번갈아가며
    넘어가는 과정에서 서로 다른 두 개의 객체가 만들어지는 상황이 발생할 수 있다.

멀티스레딩 문제 해결 방법

1. getInstance()를 동기화 시키기

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton uniqueInstance;
private Singleton(){}

public static synchronized Singleton getInstance() {
// 동기화가 필요한 시점
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
// 이미 객체가 만들어진 후에는 동기화가 필요 없다.
return uniqueInstance;
}
}
  • 간단하게 getInstance() 메서드에 synchronized 키워드를 추가한다.
  • 한 스레드가 메서드 사용을 끝내기 전까지 다른 스레드는 기다려야 하므로
    두 개 이상의 스레드가 동시에 실행되는 일을 방지할 수 있다.

동기화가 아깝다.

  • 속도 저하가 일어날 수 있고, 동기화가 아깝다는 느낌이 들 수 있다.
  • 여기에서 동기화가 아깝다는 의미는 동기화가 필요한 시점은 오직 인스턴스가 생성되기 전을 말한다.
  • 인스턴스가 이미 만들어진 후에는 동기화가 더 이상 필요 없다.

2. 인스턴스를 필요할 때 생성하지 않고, 처음부터 만들기

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
// 정적 초기화 부분에서 singleton의 인스턴스를 생성한다.
private static Singleton uniqueInstance = new Singleton();

private Singleton() {}

public static Singleton getInstance () {
// 이미 인스턴스가 있으므로 리턴만 하면 된다.
return uniqueInstance;
}
}
  • getInstance()가 호출되는지와 관련 없이 클래스가 로딩될 때,
    무조건 Singleton 인스턴스가 생성된다.
  • 싱글톤 객체가 있든 없든 관계없이 클래스 로딩 시점에 무조건 생성되기 때문에
    메모리를 항상 차지하고 있어서 비효율적이다.

3. DCL(Double-checking Locking)을 써서
getInstance()에서 동기화되는 부분을 줄이기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
private volatile static Singleton uniqueInstance;

private Singleton () {}

public static Singleton getInstance () {
// 동기화하지 않은 상태에서 null 체크
if (uniqueInstance == null) {
// 인스턴스가 없을 경우에만 동기화된 블럭으로 진입
synchronized (Singleton.class) {
// 다시 한 번 변수가 null인지 체크 후 인스턴스를 생성
if (uniqueInstance == null) {
uniqueInstance == new Singleton ();
}
}
}
return uniqueInstance;
}
}
  • volatile 키워드를 사용하여 인스턴스가 생성되어 있는지 확인한 후,
    생성되어 있지 않을 때만 동기화할 수 있다.
  • 자바 1.4 이전 버전에서는 사용할 수 없다.
  • 현재는 broken idom이며 권고하지 않는 방법이다.

4. Initialization on demand holder idiom(Lazy Initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
Singleton 클래스가 로딩될 때, LazyHolder 클래스의 변수인 uniqueInstance가
없기 때문에 LazyHolder 클래스는 초기화하지 않는다.
*/
public class Singleton {
private Singleton() {}

public static Singleton getInstance() {
return LazyHolder.uniqueInstance;
}
/*
LazyHolder 클래스는 LazyHolder.uniqueInstance를 참조하는 순간 클래스가 로딩
되고 초기화가 진행된다.
클래스를 로딩하고 초기화하는 시점은 thread-safe를 보장하기 때문에
volatile이나 synchrozied같은 키워드 없이도 성능을 보장할 수 있다.
*/
private static class LazyHolder {
private static final Singleton uniqueInstance = new Singleton();
}

}
  • holder를 이용한 초기화 방법으로 현재 가장 많이 사용하는 방법이다.
  • getInstance() 메서드가 처음으로 호출 될 때,
    클래스 로더에 의해 Singleton 객체를 생성하여 리턴한다.
  • LazyHolder 안에 인스턴스가 static이기 때문에 클래스 로딩 시점에 한 번만 호출된다.
  • final을 붙여서 값이 다시 할당되지 않도록 막는다.

5. Enum을 이용하기

1
2
3
4
5
6
public enum Singleton {
uniqueInstance;
public static Singleton getInstance() {
return uniqueInstance;
}
}
  • 자바 1.5버전부터 지원하는 Enum을 이용하는 방법
  • 모든 enum 타입은 프로그램 내에서 한 번만 초기화된다는 점을 이용해 싱글톤을 구현한다.

Enum 사용의 장점

  • 직렬화를 보장한다.
  • 리플렉션을 통해서 싱글톤을 깨뜨리는 공격에 안전하다.

Enum 사용의 단점

  • 싱글톤을 초기화 과정에서 다른 의존성이 끼어들 수 있는 가능성이 있다.
  • Enum의 초기화는 컴파일 타임에 결정되므로
    매번 메서드를 호출할 때 Context 정보를 넘겨야 하는 비효율적인 상황이 발생할 수 있다.

직렬화, 역직렬화

  • 직렬화: 자바 시스템 내부에서 사용되는 객체 또는 데이터를
    외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술
  • 역직렬화: 바이트로 변환된 데이터를 다시 객체로 변환하는 기술

결론

  • 성능이 중요하다면, LazyHolder 기법을 이용해본다.
  • 직렬화나 안정성이 중요하다면 Enum을 이용해본다.

Executor를 이용한 스레드 실행 제어

현업에서는 최대 스레드의 개수를 조절하기 위해
Executors.newFixedThreadPool(n)Executors.newSingleThreadExecutor()를 사용한다.

  • 고정 크기: Executors.newFixedThreadPool(n)
    • n개의 고정된 개수를 가진 쓰레드풀
    • 사용자가 정의한 개수의 작업자 스레드를 유지

  • 동적 크기: Executors.new CachedThreadPool()

    • 처리할 태스크가 있을 때 새로운 스레드를 만든다.
    • 60초 동안 작업이 없으면 Pool에서 제거한다.

  • 싱글스레드 생성자: Executors.newSingleThreadExecutor()

    • 태스크 처리를 위해서 하나의 작업자 스레드를 가진다.


더 공부해볼 것


참고 사이트