본문 바로가기

Programinng/Design Pattern

자바(Java) - Singleton Pattern , 싱글턴 패턴

싱글턴 패턴이란, 인스턴스를 하나만 만들어 사용하기 위한 패턴이다.


예를 들면 스레드 풀,  디바이스 설정 객체 등과 같은 인스턴스를 여러개 만든다면,


불필요한 자원을 사용하게 되고, 또 프로그램이 예상치 못한 결과를 낳을 수 있다.


싱글턴 패턴은 오직 인스턴스를 하나만 만들고 그것을 계속해서 재사용한다.



Singleton이라는 클래스를 하나 생성하겠다.

public class Singleton {
 private Singleton() {
 }
}

우선 가장 먼저 해야 할것은 생성자를 private으로 만드는 것이다.

이렇게 하므로써, 프로그래머가 실수로 new를 사용하여, 또 다른 인스턴스를 생성하는 일을 막을 수 있다.


public class Singleton {
 
 private static final Singleton instance = new Singleton();
 
 private Singleton() {
 }
 
 public static Singleton getInstance(){
  return instance;
 }
}

instance 라는 static final 변수를 초기화 한다. 

이 인스턴스는 getInstance라는 메소드에 의해서 계속 같은 인스턴스를 반환한다.


하지만, static final로 선언하면, 이 인스턴스는 클라이언트에서 사용되지도 않았는데


미리 생성되어, 자원을 낭비할 수 있다. 비관적이지만, 이러한 클래스가 수백개가 있고,


클라이언트는 이러한 클래스를 사용하지 않고 작업이 종료 된다면, 괜한 자원만 소비된 샘이다. 


게으른 초기화 (Lazy Initialization) 기법이있다. 실제로 필요할 때까지 객체를 생성하지 않으므로


불필요한 부하가 걸리지 않는다.

public class Singleton {
 
 private static Singleton instance;
 
 private Singleton() {
 }
 
 public synchronized static Singleton getInstance(){
  if(instance == null){
   instance = new Singleton();
  }
  return instance;
 }
}

위와 같이 바꾼다면, 이제 클라이언트에서 이 클래스를 처음 사용(getInstance 메소드 호출)할때

인스턴스를 생성하고 다음 부터는 처음 생성된 인스턴스를 계속 반환할 것이다.


Multi Thread 상황에서 Thread Safe 하기 위해서  getInstance 메소드를 동기화 해준다.


하지만, 이러한 동기화 방법은 성능에 문제를 준다.

처음 인스턴스를 생성할때만 동기화 해주면 되는데,

위와 같은 경우는 여러 스레드가 getInstance 메소드를 호출 할때마다 동기화를 얻기 때문에

정말 최악의 성능을 보일 수 있다. 


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;
 }
}

DCL(Double-Checking Locking) 을 사용하여, 인스턴스가 null인지 확인 한 후, null이면 

동기화를 얻고 객체를 생성한다. 그리고 그 다음부터는 동기화를 하지 않아도 된다. 


하지만 DCL은 메모리 모델, 최적화(재배치), 리오더링(reordering)과 같은 복잡한 문제로 동작하지 않을 수 있다. 그렇기 때문에 vloatile키워드를 이용한다.


이젠 하나의 인스턴스만 리턴하는 완벽한 싱글턴 패턴이 되었다.

(과연 .. 완벽할까..?)


//글을 쓰다 귀찮아서 의문점을 던지고, 쉬고있다 다시 쓰기 시작했다. 


음...이제 오로지 하나만 있는 인스턴스일까? 

답은 아니다. 자 이제 저 싱글턴을 깨보자.


먼저 리플렉션(Reflection)을 사용해서 깨보겠다.

Singleton single = Singleton.getInstance(); Constructor constructor = Singleton.class.getDeclaredConstructors()[0]; constructor.setAccessible(true); Singleton single2 = (Singleton) constructor.newInstance(null); System.out.println(single == single2);


헤쉬코드 비교 결과 'false' 성공이다.

위와 같은 리플렉션을 방어 하고 싶다면 생성자를 오버라이드하여 Exception을 날리면 된다.



Singleton을 깨는 또 한가지 방법은 바로 직렬화(Serialization)다.

직렬화 후 역직렬화(Deserialization)되면, 여러개의 인스턴스를 생성할 수 있다.

이 역시 방어 하고 싶다면, readResolve()를 정의한다.


private Object readResolve() throws ObjectStreamException{ return instance; } 위와 같이 readResolve 메소드를 정의해 놓으면, 역직렬화가 되었을때 자동으로 이 메소드가 호출

된다.  



저 두가지 귀찮은 방법 말고, 아래처럼 Singleton을 Enum으로 구현하면 위와같은 문제점을 간단

히 해결할 수 있다.

enum Singleton{ INSTANCE }