싱글톤 패턴
싱글톤 패턴이란
- 애플리케이션에서 어떤 클래스의 인스턴스가 단 하나만 존재해야 하는 경우이다.
- 인스턴스를 단 하나만 생성해야 하고,
- 그 인스턴스에 대한 글로벌 접근을 제공해야 한다.
- 싱글톤 패턴의 장점은
- 모든 사용자에게 같은 같은 인스턴스 제공이 필요할 때 사용된다.
- 인스턴스를 하나만 만드니 메모리 절약이 된다.
가장 단순한 구현
싱글톤 패턴의 필수 조건을 만족하는 단순한 구현을 알아보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SimpleSingleton {
private static SimpleSingleton instance;
private SimpleSingleton() {}
public static SimpleSingleton getInstance() {
if(instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
- 가장 기본적인 싱글톤의 필수 조건을 둘 다 만족하는 코드이다.
- 단 하나의 인스턴스만 만들어지고,
- 글로벌 접근을 제공한다.
하지만 이 방법은 안전할까?
멀티 스레드 환경을 생각해보자.
1
2
3
4
5
6
7
8
9
public static SimpleSingleton getInstance() {
if(instance == null) { // a
instance = new SimpleSingleton(); // b
}
return instance;
}
- 1번 스레드와 2번 스레드가 있다고 할 때,
- 1번 스레드가 a에 접근한다.
- 어 인스턴스 없네? 만들어야겠다.
- 1번 스레드가 b에, 2번 스레드는 a에 접근한다.
- 1번 스레드 : 새 인스턴스를 만들어야ㅈ..
- 2번 스레드 : 어 인스턴스 없네? 만들어야겠다.
- 1번 스레드 인스턴스A 생성 완료.
- 2번 스레드 인스턴스B 생성 완료.
- 1번 스레드가 a에 접근한다.
- 하나의 인스턴스만 만들어야 하는 싱글톤 패턴이 깨져버린걸 알 수 있다.
쓰레드 세이프한 방법
보다 안전하고 확실한 싱글톤 패턴을 위해 Thread-Safe하게 코드를 짜야한다.
가장 간단한 Thread-Safe
- synchronized 블럭을 사용해 동기화 처리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ThreadSafeSingleton1 {
private static ThreadSafeSingleton1 instance;
private ThreadSafeSingleton1() {}
public static synchronized ThreadSafeSingleton1 getInstance() {
if(instance == null) {
instance = new ThreadSafeSingleton1();
}
return instance;
}
}
- 장점
- 쉽게 구현할 수 있다.
- 확실한 Thread-Safe
- 단점
- 동기화 처리를 계속 해야하기 때문에 성능 저하
이른 초기화
- Eager initialization으로 컴파일 시점에 그냥 바로 인스턴스를 만들어버린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class ThreadSafeSingleton2 {
private static ThreadSafeSingleton2 instance = new ThreadSafeSingleton2();
private ThreadSafeSingleton2() {}
public static ThreadSafeSingleton2 getInstance() {
return instance;
}
}
- 장점
- 단순하다.
- 확실한 Thread-Safe
- 단점
- 인스턴스를 미리 만드는게 단점
- 메모리를 잡아먹는데 클래스마다 다르지만 부담되는 수준일 수도 있다
- 근데 아무도 안쓴다? 메모리 낭비임
- 인스턴스를 미리 만드는게 단점
double check locking
- 인스턴스가 비었는지 두 번 체크한다.
- 인스턴스를 만드는 시점에 동기화를 때린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ThreadSafeSingleton3 {
private static volatile ThreadSafeSingleton3 instance;
private ThreadSafeSingleton3() {}
public static ThreadSafeSingleton3 getInstance() {
if(instance == null) { // a
synchronized (ThreadSafeSingleton3.class) {
if(instance == null) { // b
instance = new ThreadSafeSingleton3();
}
}
}
return instance;
}
}
- a와 b에서 볼 수 있듯이 두 번의 null 체크가 있다.
- 가장 최초의 인스턴스 생성 시점에서만 동기화가 일어나고,
- 그 뒤로는 첫번째 a 체크에서 false이기 때문에 동기화가 일어날 일이 없다.
해당 코드는 volatile 키워드가 도입된 jdk 1.5부터 사용이 가능하다.
- 장점
- 동기화를 최대한 적게 할 수 있다.
- Lazy Initialization
- 단점
- volatile 키워드를 사용하니까 CPU Cache를 쓰지 못하는 것 때문에 변수 접근이 느려질 수 있다는 점
static inner class
- 스태틱 이너 클래스의 특징
- 본인을 감싸는 클래스(A)와 스태틱 이너 클래스(B)의 존재는 완전히 독립적이다.
- A가 생성되었다고 B가 생성되는 것이 아니고, 그 반대도 마찬가지이다.
- B가 생성되는 시점은 오직 B의 정적 필드에 대한 접근이나, 정적 메서드가 호출된 경우이다.
- 본인을 감싸는 클래스(A)와 스태틱 이너 클래스(B)의 존재는 완전히 독립적이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ThreadSafeSingleton4 {
private ThreadSafeSingleton4() {}
private static class InstanceHolder {
private static final ThreadSafeSingleton4 INSTANCE = new ThreadSafeSingleton4();
}
public static ThreadSafeSingleton4 getInstance() {
return InstanceHolder.INSTANCE;
}
}
- 장점
- Lazy Initialization
- 스태틱 이너 클래스에 접근하는 시점에 생성되기 때문
- Thread-Safe
- INSTANCE는 static이기 때문에, 클래스 로딩 시점에 단 한 번만 초기화 되기 때문에 스레드 세이프할 수 있다.
- Lazy Initialization
더 고려해야 할 것들
여태 열심히 배우고 따라 구현했던 것들은 사실 사용하는 쪽에서 강제로 깰 수 있는 방법이 있다.
- 리플렉션
- 직렬화와 역직렬화 알아보고 얘네마저 막을 수 있는 방법을 알아본다.
리플렉션을 사용한 싱글톤 터치기
1
2
3
4
5
6
7
8
9
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton1 == singleton2); // false
- 리플렉션으로 숨겨뒀던 생성자 함수를 끄집어내서 억지로 싱글톤을 깨트린 것을 볼 수 있다.
리플렉션으로부터 싱글톤을 지키는 방법
1
2
3
4
5
public enum Singleton {
INSTANCE
}
- 열거형 클래스를 통해 방어할 수 있다.
- 열거형 클래스는 리플렉션으로 생성하는게 막혀있음.
- 단, 컴파일 시점에 인스턴스가 미리 만들어진다는 단점이 있다.
- 또 enum의 특성상 상속을 사용할 수 없다는 점도 있다.
직렬화와 역직렬화로 싱글톤 깨트리기
1
2
3
4
5
class Singleton implements Serializable { // 이렇게..
// ...
}
- 우선 사용하고자 하는 싱글톤 클래스에 Serializable 마커 인터페이스를 붙여줘야 한다.
1
2
3
4
5
6
7
8
9
10
11
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test"));
ObjectInputStream in = new ObjectInputStream(new FileInputStream("test"));
Singleton singleton1 = Singleton.getInstance();
out.writeObject(singleton1);
Singleton singleton2 = (Singleton) in.readObject();
System.out.println(singleton1 == singleton2);
- 역직렬화 시에 생성자를 사용해서 새 객체를 만들어서 반환하기 때문에 싱글톤이 깨진다.
역직렬화 시에도 싱글톤을 지키는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton implements Serializable {
// ...
public static Singleton getInstance() {
// ...
}
public Object readResolve() {
getInstance();
}
}
- Object readResolve() 메서드를 정의해주면 해결된다.
- 얘는 역직렬화 시에 어떻게 값을 전달할지 정의해줄 수 있는 메서드이다.
- 참고
- 리플렉션으로부터 싱글톤을 지켰냈던 방법인 열거형 클래스로 역직렬화 시에도 사용할 수 있다.
- enum은 기본적으로 Serializable이 붙어 있음
This post is licensed under CC BY 4.0 by the author.