Post

싱글톤 패턴





싱글톤 패턴이란

  • 애플리케이션에서 어떤 클래스의 인스턴스가 단 하나만 존재해야 하는 경우이다.
    • 인스턴스를 단 하나만 생성해야 하고,
    • 그 인스턴스에 대한 글로벌 접근을 제공해야 한다.
  • 싱글톤 패턴의 장점은
    • 모든 사용자에게 같은 같은 인스턴스 제공이 필요할 때 사용된다.
    • 인스턴스를 하나만 만드니 메모리 절약이 된다.





가장 단순한 구현

싱글톤 패턴의 필수 조건을 만족하는 단순한 구현을 알아보자.

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. 1번 스레드가 a에 접근한다.
      • 어 인스턴스 없네? 만들어야겠다.
    2. 1번 스레드가 b에, 2번 스레드는 a에 접근한다.
      • 1번 스레드 : 새 인스턴스를 만들어야ㅈ..
      • 2번 스레드 : 어 인스턴스 없네? 만들어야겠다.
    3. 1번 스레드 인스턴스A 생성 완료.
    4. 2번 스레드 인스턴스B 생성 완료.
  • 하나의 인스턴스만 만들어야 하는 싱글톤 패턴이 깨져버린걸 알 수 있다.





쓰레드 세이프한 방법

보다 안전하고 확실한 싱글톤 패턴을 위해 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의 정적 필드에 대한 접근이나, 정적 메서드가 호출된 경우이다.
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이기 때문에, 클래스 로딩 시점에 단 한 번만 초기화 되기 때문에 스레드 세이프할 수 있다.





더 고려해야 할 것들

여태 열심히 배우고 따라 구현했던 것들은 사실 사용하는 쪽에서 강제로 깰 수 있는 방법이 있다.

  • 리플렉션
  • 직렬화와 역직렬화 알아보고 얘네마저 막을 수 있는 방법을 알아본다.



리플렉션을 사용한 싱글톤 터치기

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.