본문 바로가기

Design Pattern

[디자인패턴] Singleton Pattern (싱글톤 패턴)

  • 싱글톤 패턴의 목적과 적용시기
  • 단일 쓰레드에서의 싱글톤 패턴 구현
  • 다중 쓰레드에서의 싱글톤 패턴 구현
  • 관련된 패턴들

 

  • Usage of Singleton Pattern

Purpose

    - 시스템에서 클래스의 객체가 단 한 개만 생성되도록 하려할 때

Use When

    - 클래스의 객체가 하나만 필요할 때 (ex. Window Manager, Printer Spooler, Thread Pool Manager..)

 

 

 


 

  • Singleton pattern implementation for single-thread

 

생성자를 private로 해서 외부에서 new로 생성 못하도록 하고 getInstance() 메소드를 통해 객체를 생성하도록 한다.

getInstance()가 호출되면 먼저 보유중인 참조변수가 null인지 검사하여

if) uniqueInstance == null -> 객체 생성,  생성된 객체의 참조변수를 멤버변수로 보유

 

else) uniqueInstance != null -> 보유하고 있던 참조변수를 return 

 

    → Problem) 다중 쓰레드에서 올바르게 동작하지 않는다.

 

두 쓰레드 모두 unique==null 상태일때 if문 안으로 들어옴 -> 둘다 new로 객체를 생성하려함 -> 한놈이 만들고 또 다른놈도 그 후에 만들게 돼서 결국 두개의 객체 생성 -> 두 쓰레드가 서로다른 객체를 참조하게 됨

 

 

 


 

  • Singleton pattern implementation for multi-thread

위의 다중 쓰레드에서의 문제를 몇 가지 방법으로 해결해보고자 한다.

 

Option1) getInstance() 메소드에 Lock!!

synchronized를 통해 메소드 자체에 Lock을 걸도록 한다. 따라서 두 개 이상의 쓰레드가 해당 메소드안에 동시에 존재하지 못하게 한다.

 

Thread A가 먼저 getInstance() 메소드 안에 들어가면 Thread B, C .. 다른 쓰레드들은 A가 나올때까지 기다리고 있다가 들어가야한다.

 

 

 

 

    → Problem) 정확성 Ok, But 너무 지나친 Lock! 속도 부하가 심하다.

 

 

 

Option2) 클래스가 JVM에 올라감과 동시에 객체를 생성해버려!!

멤버변수 = new Singleton() 를 통해서 처음부터 아예 객체를 생성하고 그 후에 getInstance() 는 단지 멤버변수를 return 해주기만한다.

 

 

 

 

 

    -> Problem) 정확성, 속도 Ok, But 무조건 객체가 생성되어 프로그램 시작~종료까지 메모리에 항상 상주 (아무도 객체를 생성하지 않는 불필요한 경우에도 메모리 overhead..)

 

 

 

Option3) Double-Checked Locking!! (null인지 두 번 check하자)

null인지 한 번 검사하고 실제로 객체 생성하기 전에 Lock을 걸고 -> 다시 한번 더 검사하고 -> 객체 생성

 

한번 더 검사하는 이유 : Thread A, B 모두 null 상태에 검사해서 if 문 안으로 들어왔는데 A가 Lock 걸고 객체 생성하고 나옴. 이때 B도 이미 if 문 안에 들어와있는 상황이므로 Lock 걸고 객체 생성하려함. 따라서 실제로 생성하기 전에 한번 더 검사해야한다.

 

 

 

 * 첫번째 if문은 정확도를 위함이아니라 속도향상을 위한 것이다. 무조건 Lock걸지 않고 null 이 아니라면 그냥 return 하도록. But null 아니면 Lock걸고 그 안에서 한번 더 검사

 

    -> Problem) 속도, 메모리 모두 Ok! But 정확성도 Ok 처럼 보였지만 사실 아니였다...

 

 

  • Double-Checked Locking Issue

정확성에 문제가 없어보였지만 아주 가끔 에러가 발생함을 발견됐다. 그 이유는 Thread A, B가 있을때 A가 new를 통해 객체를 생성한 후 B가 이를 참조하려할때 JVM이 객체 생성이 완성되기도 전에 B에게 참조변수를 전달해주는 경우가 생겼기 때문이다. 이는 메모리에서 캐시 속도향상을 위해 각 쓰레드에게 객체를 원본 그대로 전달하는게 아닌 copy를 만든 후 전달해주기 때문이다. 따라서 완성이 다 되기도 전에 copy를 만든 후 B에게 참조변수로 전달해주는 경우가 생긴다.

 

    => Solution : volatile

 

 

 

Option4) Double-Checked Locking!! + volatile

volatile은 공유자원에 대해서 copy를 만들지 말라고 명시하는 것이다. 따라서 모든 멀티 쓰레드들이 공유자원을 같은 수준으로 보게끔 한다.

 

이제 Thread A가 new를 통해 객체를 생성하다가 B에게 차례가 넘어가 첫번째 if 조건문을 실행해도 copy를 갖고 있지 않기때문에 이전처럼 null 검사에서 실패하여 불완전한 참조변수를 반환받는 경우가 생기지 않는다.

 

 

 

 

 

Volatile Property (Synchronized 역시 보장해주는 속성)

 

 

1. Atomicity : 32bit 정수에서 16bit까지 수정한 상태인데 중간에 다른 thread가 가져가서 읽는 경우 방지

(atomic하게 read / write하는 경우)

 

2. Visibility : 공유 자원에 대해서 변화가 생기면 모든 thread가 같은 수준으로 볼 수 있게 해야한다.

 

3. Ordering : 명령문의 순서를 보장해줘야 한다. 

(ex. a=1; b=3; c=b; 에서 b=3과 c=b의 순서는 보존돼야 한다.)

read() 에서 if (x < y) 문이 절대 실행되지 않을 것 같지만 Ordering이 보장되지 않는 경우, 멀티 쓰레드 환경에서 y=50을 먼저 작성하여서 x < y 가 만족하는 경우가 생길 수 있다.  

 

 

 

 

 

 

 

 

 

 


 

  • Related patterns

Abstract Factory, Builder, 그리고 Prototype 도 Singleton pattern을 자신들의 구현에 적용하기도 한다.

Facade의 경우 대부분의 경우 Singleton 이다.

State Object경우 대부분 Singleton이다.