본문 바로가기

Design pattern

[JAVA 디자인패턴] Singleton pattern(싱글턴 패턴)

Singleton pattern(싱글턴 패턴)

인스턴스가 오직 한 개만 생성되는 것을 보장하고, 어떤 위치에서든 접근할 수 있도록 하는 디자인 패턴이다. 싱글턴은 수학용어로서, 단 하나의 원소를 가진 집합에서 유래하였다.

싱글턴 패턴을 이해하기 위해, 프린터 관리자를 만들어 보자.

프린터는 오직 한 개만 존재하는 상황이다. 그렇다면, 생성자도 오직 한 번만 실행되어야한다. 그래서 생성자는 public이 아닌, private으로 한다. 그 후, 다른 메소드가 생성자를 1번만 실행하게 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Printer{
 
    private Printer printer = null;
 
    private Printer(){ }
 
    public Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }
 
    public void print(String msg){
        System.out.println(msg);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

하지만, 위의 방법을 사용하면 문제가 발생한다. getPrinter를 실행하려면, 객체를 생성해야 한다. 하지만, 싱글턴의 원칙을 지키기 위해서 new연산자를 private으로 설정해야 한다.

이를 해결하기 위해서, 객체 생성 여부와 무관하게 getPrinter메소드를 실행해야 한다. 즉, 정적 메소드를 사용하면 된다(static)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Printer{
 
    private static Printer printer = null;
 
    private Printer(){ }
 
    public static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }
 
    public void print(String msg){
        System.out.println(msg);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

static을 사용해서 정적 메소드로 만들었다. 그러면, 객체 생성 여부와 관계없이 getPrinter메소드를 이용해서 객체의 레퍼런스를 받아올 수 있다.

하지만, 위의 코드도 문제가 발생한다. 바로 race condition이다. 스레드들이 동시에 getPrinter에 접근한다면, 어떻게 될까?

스레드 5개가 getPrinter를 호출하고, 호출한 프린터에 대해서 hashCode를 찍어보았다. 본래 기대하는 결과는, 생성자가 1번만 실행되어 모두 같은 hashCode가 나와야 한다. (같은 객체를 사용) 하지만, 해당 구역이 critical section으로 설정되어 있지 않다. 그래서 race condition이 발생하였다. 그래서 new 연산이 중복되어서 발생하였다.

그렇다면, Thread asynchronous문제를 해결하기 위해서 race condition이 발생이 예상되는 부분에 critical section을 설정해주면 된다. 즉, 자바에서synchronized 키워드를 이용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Printer{
 
    private static Printer printer = null;
 
    private Printer(){ }
 
    public static synchronized Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }
 
    public void print(String msg){
        System.out.println(msg);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

critical section 설정을 해줌으로서, 해당 구역은 복수의 스레드가 한번에 접근하지 못한다. 그래서 처음 thread가 critical section에 들어갈 때 new 연산을 통해서 Printer 객체가 생성된다. 그리고 다음에 접근하는 thread들은 이미 인스턴스가 존재하기 때문에 해당 레퍼런스만 받아서 이용한다.

하지만, critical section을 이용하면 운영체제의 자원을 낭비하게 된다(spinlock을 이용한다면, lock연산을 반복적으로 한다. mutex라면, context switch) 그래서 critical section을 사용하지 않을 수 있다.

 

그래서 synchronized 키워드를 제거해서 싱글턴을 만들 수 있는 방법을 생각해보자.

 

1
2
3
4
5
6
7
8
9
10
11
public class Printer{
 
    private static Printer printer = new Printer();
 
    private Printer(){ }
 
    public static Printer getPrinter(){
 
        return printer;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

Printer 클래스가 외부에서 참조된다면, Printer.class파일은 JVM에 올라가게 된다. 그리고 new Printer()연산을 실행하게 된다. 그 후, new 연산은 실행하지 않기 때문에 한 개의 인스턴스만 생성되고 그것만 참조한다.(printer 멤버는 처음부터 올라가 있고, null로 초기화된 상태에서 참조되어 new Printer()가 실행된다.)

하지만, 위의 상황에서 아주 작은 문제 하나가 있다. 무조건 해당 객체가 생성된다는 것이다. Printer 객체에서 정적 메소드를 만들었다. 그런데 그 정적 메소드는 printer 멤버를 사용하지 않는다. 즉, 필요하지 않는데 무조건 printer 객체가 생성된다. (사실 크게 크리티컬한 문제는 아니다)

 

critical section문제를 해결하면, OS의 자원 낭비라는 단점이 생기고, 클래스의 로딩 시점을 이용해서 멤버 변수를 처음부터 생성해서 바인딩 해주면, 무조건적인 객체 생성이라는 단점이 발생한다. 이와같은 문제를 해결하기 위해서 Initialization on demand 라는 방법을 이용한다. 실제로 이 방법을 많이 쓴다고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Printer {
 
    //private static Printer instance = new Printer();
 
    private Printer(){}
 
 
    public static class LazyHolder{
        public static final Printer instance = new Printer(); 
    }
 
    public static Printer getPrinter(){
        return LazyHolder.instance;
    }
 
    public static void func(){
        //......
    }
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

위의 방법은 os자원 낭비의 문제 + 무조건적인 바인딩 생성 문제를 해결해준다. 만약 func라는 정적 메소드를 사용한다면, Printer라는 객체를 생성하지 않고 쓸 수 있다. 그러다가 getPrinter라는 정적 메소드가 실행된다면, LazyHolder라는 클래스가 JVM에 올라간다. 즉, LazyHolder라는 클래스에서 멤버 변수 instance에 new Printer 연산이 실행되어 바인딩된다. 이러한 방법을 이용하면 필요할 때만 new 연산을 통해 객체가 생성된다.

이러한 방법을 이용해서 os의 자원 낭비 문제와 무조건적인 바인딩 생성문제를 해결할 수 있다.

 

참고문헌

JAVA 객체지향 디자인 패턴, 정인상