본문 바로가기
공부(~2016)/Java

[Java] Singleton 패턴 구현 예제

by soy; 2015. 1. 25.

싱글톤 패턴

클래스의 인스턴스가 반드시 딱 한 개만 생성됨을 보장하는 패턴

- 클래스의 생성자를 private로 선언하여 외부에서 인스턴스를 생성하지 못하도록 방지함 -> 생성자가 private 이므로 상속이 불가능함

- 클래스 내에 private static 변수로 자기 자신 클래스의 인스턴스를 가짐

- 이 private static 변수를 리턴해주는 public static getInstance() 메소드를 가짐


* 이 포스팅에서는 Lazy 초기화를 이용한 예제만 다룬다. 필드 선언과 함께 초기화하는 것은 다루지 않음


첫 번째 예제 - Single Threaded Execution -> 문제 없음

import java.util.Date;
 
public class MySystem {
    private static MySystem instance = null;
    private Date date = new Date();
    private MySystem() {
    }
    public Date getDate() {
        return date;
    }
    public static synchronized MySystem getInstance() {
        if (instance == null) {
            instance = new MySystem();
        }
        return instance;
    }
}

getInstance 메소드를 synchronized로 선언함. 

instance 필드의 값이 null이라는 것은, getInstance 메소드가 최초로 실행되었음을 의미.

getInstance 메소드가 최초 실행되었을 때에 인스턴스를 생성한다.

만약 두 개의 쓰레드가 동시에 getInstance 메소드를 호출하더라도, synchronized로 선언되었기 때문에 인스턴스는 한 번만 생성됨을 보장할 수 있음.

단 인스턴스가 생성된 이후에도 getInstance 메소드를 동시에 여러 쓰레드에서 호출하지 못하므로 성능이 떨어짐.

getInstance 메소드가 한 번도 호출되지 않았다면 인스턴스가 생성되지 않으므로 메모리를 아낄 수 있다. 



두 번째 예제 - Double-Checked Locking -> 문제 있음

import java.util.Date;
// 이 코드는 thread-safety 하지 않다
public class MySystem {
    private static MySystem instance = null;
    private Date date = new Date();
    private MySystem() {
    }
    public Date getDate() {
        return date;
    }
    public static MySystem getInstance() {
        if (instance == null) {                 // (a) 첫번째 test
            synchronized (MySystem.class) {     // (b) synchronized 블록에 들어감
                if (instance == null) {         // (c) 두번째 test
                    instance = new MySystem();  // (d) 인스턴스 생성
                }
            }                                   // (e) synchronized 블록에서 나옴
        }
        return instance;                        // (f)
    }
}

첫번째 예제에서 성능을 높이기 위해 수정한 에제.

instance 필드의 값이 null일 때에만 synchronized 블록에 들어간다. 즉 getInstance 메소드가 두번째 호출될때부터는 synchronized 블록에 들어가지 않는다.

하지만 이 예제는 정상적으로 동작하지 않을 가능성이 있다.

메인에서 MySystem.getInstance().getDate()를 호출했을 때, data 필드가 초기화 되어 있지 않을 가능성이 있다.


두 개의 쓰레드 A, B가 동시에  MySystem.getInstance().getDate() 를 실행했다고 하자. 그리고 다음과 같이 동작하였다고 하자.


1) 쓰레드 A가 (a), (b), (c), (d) 까지 실행을 한다.

2) 쓰레드 A에서 B로 switching 된다.

3) 쓰레드 B가 (a)를 실행하고, instane != null 으로 판단하여 (f)를 실행한다.

4) 쓰레드 B가 getData()를 실행한다.

-> 이 때, 쓰레드 A는 아직 synchronized 블록에서 나오지 않았고 쓰레드 B는 synchronized 블록에 아예 들어가지 않았다. 이런 경우 쓰레드 A가 생성한 MySystem 인스턴스의 date 필드 값이 쓰레드 B에는 보이지 않을 수 있다.

-> 만약 쓰레드 B 또한 synchronized 블록에 들어가서 (a), (b), (c), (f)를 실행했다면 문제가 없었을 것이다.


* 쓰레드 B에 instance 필드의 값은 보이는데 data 필드의 값이 보이지 않는 이유는 무엇일까..? reorder에 의해 instance 필드의 값이 data 필드의 값보다도 먼저 보일 가능성이 있다는 것이다.


* instance 필드를 volatile으로 선언하면 이런 문제는 없어진다. 하지만 이 예제는, synchronized 메소드를 사용한 첫번째 예제의 성능을 개선하기 위한 것이다. 만약 volatile으로 선언한다면 첫 번째 예제(synchronized 메소드를 사용하는 것)와 성능이 별반 다를 게 없다.



세 번째 예제 - Initialization On Demand Holder -> 문제 없음

import java.util.Date;
 
public class MySystem {
    private static class Holder {
        public static MySystem instance = new MySystem();
    }
    private Date date = new Date();
    private MySystem() {
    }
    public Date getDate() {
        return date;
    }
    public static MySystem getInstance() {
        return Holder.instance;
    }
}


이 예제는 정상적으로, 안전하게 동작한다. 또한 synchronized나 volatile을 사용하지 않기 때문에 수행 능력이 떨어지지 않는다.

MySystem 클래스의 inner static 클래스로 Holder 클래스를 둔다. 

이 Holder 클래스는 instance 필드를 갖는다. instance 필드는 선언시에 초기화하였다.

getInstance 메소드의 리턴값은 Holder.instance이 된다.


세번째 예제에서는 Holder 클래스의 "클래스 초기화"를 이용하여 thread-safety한 싱글톤 인스턴스를 만들고 있다. (클래스의 초기화는 Java 사양상 thread-safety하다.)

또한 lazy initialization을 이용하고 있다. Holder 클래스의 초기화는 쓰레드가 이 클래스에 접근했을 때 비로소 실행된다. 즉 MySystem.getInstance 메소드를 호출하기 전까지는 Holder 클래스의 초기화도 이뤄지지 않고 나아가 MySystem 인스턴스도 생성되지 않는다. 이 방법을 사용하면 메모리를 불필요하게 사용하는 일도 없어진다.


댓글