개발 이야기

kris.j 2012. 8. 1. 19:29

필자가 2009년쯤에 네이버에 작성한 포스팅인데.. 다음으로 이사오면서 말투가 좀 섞여서 있을수 있으니


읽으시는 분들은 이해하시기 바란다..


제목은 열라 거창 하지만 알고 보면 별거 없다..


어떻게 하면 잘 사용하는 것인지 알려주겠다..


이건 책에도 안나온다..


아. 모르겠다. 한 3년전에 유명한 책들 몇개 뒤졌는데 잘 안나오더라. 하지만 지금은 나올지도..


보통 알고들 계시는 2가지 방법과 특별한 한가지 방법, 그리고 현재까지 알려진 방법중에


가장 사용할만한 방법에 대해 설명 들어간다.


1. 첫번째 방법 클래스 생성과 동시에 초기화 하기

   - 가장 기초적이면서 가장 많이 사용하는 코드.

- Code 1. 클래스 로딩시 인스턴스 생성(JVM에 로딩)
public class Singleton1 {
 private static Singleton1 singleton = new Singleton1();
 public static Singleton1 getInstance(){
  return singleton;
 }
 private Singleton1(){
 }
}


클래스가 JVM에 로딩되는 시점에 인스턴스가 생성되며 보통 인스턴스의 생성시간이 짧고 인스턴스의 크기가 크지 않은 경우 사용한다.


이거 많이들 쓴다..


그리고 괜찮은 접근 방식이다..

단!!!!!!!!!!!!!!!!!!!!!

해당 클래스의 인스턴스를 초기화 하는 시간이 길면 다들 저 객체를 달라고 아우성을 치면서 Blocking이 걸릴테니까...


static의 경우 GC에 포함되지 않을 뿐더러 Instance의 크기가 큰데 이거 썻다간 JVM이 OOM뱉어내고 운명하실지도 모른다...


뭐 어차피 위와 같은 상황이라면 OOM은 고려 대상이 아니겠지만서도..


왜 static이 GC에 포함되지 않는지 간단히 설명하면.


heap에 할당되지 않고 Class Area에 할당 되기 때문이다..


이 이상 알고싶거든 다른 포스팅을 찾아보시기 바란다..


2. 두번째 방법 클래스의 메서드가 호출될때 생성

  - 아~ 나는 조금 잘난 개발자야.. 그리고 난 똑똑해 라고 생각하시는 분들이 많이들 쓰시더라...

- Code 2. 메서드 호출시 인스턴스 생성, 늦은 초기화사용
public class Singleton2 {
    private static Singleton2 singleton;
    public static synchronized Singleton2 getInstance(){  <== 요기 1번
        if (singleton == null) {
            singleton = new Singleton2();
        }
        return singleton;
    }
    private Singleton2(){
    }
}


이게 잘 짠 코드로 보이지만 절때!! 네버!! 잘짠 코드가 아니다..


왜????


아래 설명한다..


Code2의 경우 메서드 호출시마다 동기화가 일어나므로 동기화비용이 추가 발생하여 늦은 초기화의 잇점을 얻기가 힘듦.

동기화 구역을 최소화 하기 위하여 DCL(Double checked locking) 기법을 사용한다.

라고 하여 아래의 DCL방법이 나타났으니...

자 한글로 읽으면 더블 체크드 락킹 되시겠다..


근데 말이지 DCL을 쓰는 궁극적인 이유는 늦은 초기화

즉, JVM이 로딩 되는 시점이 아닌 실제로 사용되는 시점에 초기화 하겠다~~ 라는 이유 때문인데..


늦은 초기화는 아래와 같은 경우에사용합니다.(http://www.javapractices.com/topic/TopicAction.do?Id=34 인용)
Lazy initialization of a field may be useful when both of these conditions are true :
the field is particularly expensive to create
the field has an optional character, in the sense that it may or may not be of interest to the caller

즉, 필드의 생성비용(생성시간)이 큰 경우이거나, 필드의 값이 반드시 사용되지 않을수도 있는경우.


과연 실제로 이것과 일치하는 Case가 얼마나 될지 개발 12년 넘게 해온 나도 거의 못봤는데...


어찌 되었건, 그래서, 어느 똑똑하신 분이 혹시라도 모를 100만분의 1의 케이스에 해당하는 개발자들을 위해 

은혜를 배푸사 아래와 같은 DCL기법을 통해서 늦은 초기화도 하면서 동기화 overhead Issue도 없앨 수 있는

아주 기똥찬 기법을 고안하셨다 이거지..


3. DCL(Double checked locking)적용 Code

public class Singleton3 {
    private volatile static Singleton3 singleton;
    public static Singleton3 getInstance(){
        if (singleton == null) { // point 1
            synchronized(this) {
                if (singleton == null) {
                    singleton = new Singleton3(); // point 2
                }
            }
        }
        return singleton; // point 3
    }
    private Singleton3(){
    }

}


아~~~ 근데 이것도 잘못된 구현체에 포함된다지 뭐야...


Code3의 경우는 최초 getInstance()메서드가 호출되는 시점에 객체가 생성되고 두번째 호출부터는 point 1의 체크를 통해 동기화 블럭을 거치지 않으므로 늦은 초기화의 장점과 추가 동기화 비용이 발생하지 않도록 한다.

Code3은 이미 망가진 Idiom에 속합니다.

이유는 out-of-order write problem과 Thread 안정성 문제입니다.


일단 out-of-order의 원인은 아래와 같음.

결론만 말하면 Point 2에서 리턴된 인스턴스는 생성자의 본체가 완전히 실행된 이후의 인스턴스가 아닐수도 있다라는 건데요..

아래 문서에서도 어떠한 이유로 또한 어느 JDK에서 또는 어떤 상황에서 out-of-order write problem 발생할수 있는지에 대한 언급은 찾지 못했습니다.

해당 문서에 게시된 내용은 Sun JDK 1.2.1 JIT 컴파일러에서 생성된 어샘블리 코드를 예로 들어 설명하고 있습니다.

다른버젼의 JIT컴파일러에서도 위와같은 상황이 발생할지는 좀더 확인해봐야 할것으로 보입니다.

자세한 내용은 아래 링크 참조 하시면 됩니다.

http://www.ibm.com/developerworks/kr/library/j-dcl.html


Thread안정성 부분에서 의 이슈.

두개의 Thread T1, T2가 동시에 getInstance()메서드에 접근한 경우의 시나리오를 작성해보면

1. T1이 getInstance() 호출

2. T2가 getInstance() 호출

3. T1이 point 3를 실행하기전 대기 상태로 진입

4. T2가 point 1을 실행하는 시점에서는 singleton객체가 null이 아니므로 singleton객체 return

5. T2가 singleton를 사용하려고 하지만 해당 Constructor가 완료되지 않았으므로 정상 동작을 보장하지 않음.
 - 즉, 객체의 메모리영역만 할당 되었을뿐 초기화가 완료되지 않았으므로 정상동작하지 않음.


결국 이것도 저것도 아닌 뭘 사용해야 하느냐??


대부분의 상황에서는 1번 코드를 사용하라.


하지만 반드시 나는 100만분의 1에 해당 하므로 늦은 초기화를 사용해야겠어... 라고 하는 분들은..


SingletonHolder패턴을 사용하시면 되겠다..


코드는 아래와 같다.


public class Singleton {
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}


이게 왜 문제가 안되는지 자세한 내용을 알고 싶다면 SingletonHolder패턴을 검색하면 된다.


간단하게 설명한다..


1. final은 한번 초기화 되면 값을 변경할 수 없다.

2. static은 Class가 로딩되는(즉, Access되는) 시점에 해당 객체가 JVM의 Class Area에 저장된다.

3. Inner class를 사용함으로서 SingletonHolder.INSTANCE 이 부분이 실행되기 전까지는 2.의 법칙에 의해서 Load 되지 않는다.


위와 같은 3가지 조건에 의해서 늦은 초기화, 초기화시 안정성, Thread 안정성


세가지를 모두 잡은 case되시겠다.


이상 끝~~~

좋은 글 고맙습니다!
퍼가겠습니다~.