본문 바로가기

JAVA/Design Patterns

5. 싱글톤 패턴

반응형

싱글턴 패턴

정의 - 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴


용도

- 스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정를 처리하는 객체, 로그 객체, 디바이스 드라이버 등 인스턴스가 2개 이상만들게 되면 프로그램이 이상하게 작동하거나, 자원을 불필요하게 잡아먹거나, 결과에 일관성이 없어지는 심각한 문제가 생길수 있음


고전적인 싱글턴 패턴


public class Singleton {

    private static Singleton uniqueInstance; //유일한 인스턴스를 저장하기 위한 정적 변수

    

    private Singleton () { }  //private으로 선언하여 외부에서 객체생성 못함 Singleton에서만 클래스의 인스턴스를 만들수 있음

    

    public static Singleton getInstance() { //클래스의 인스턴스를 만들어서 리턴

        if (uniqueInstance == null) { // 인스턴스 생성되지 않았는지

            uniqueInstance = new Singleton(); //private으로 선언된 생성자를 이용해서 객체를 만든다음 대입, 인스턴스가 필요한 상황이 오기전에는 인스턴스를 생성하지 않는다. 이런방식을 게으른 인스턴스 방식 lazy instantiation

        }

        return uniqueInstance; 

    }

}



예제) 초코릿 공장


package singleton;


public class ChocolateBoiler {

    private boolean empty;

    private boolean boiled;


    public ChocolateBoiler() {

        empty = true;

        boiled = false;

    }


    public void fill() {

        if(isEmpty()) {

            empty = false;

            boiled = false;

            //보일러에 우유, 초코릿을 혼합한 재료를 넣음

        }

    }

    public void drain() {

        if(!isEmpty() && isBoiled()) {

            //끓인 재료를 다음 단계로 넘김

            empty = true;

        }

    }

    public void boil() {

        if(!isEmpty() && !isBoiled()) {

            //재료 끓임

            boiled = true;

        }

    }


    private boolean isEmpty() {

        return empty;

    }

    private boolean isBoiled() {

        return boiled;

    }

}

기본적인 초코릿공장 클래스


package singleton;


public class ChocolateBoiler {

    private boolean empty;

    private boolean boiled;

    

    private static ChocolateBoiler instance;

    

    private ChocolateBoiler() {

        empty = true;

        boiled = false;

    }


    public static  ChocolateBoiler getInstance() {

        if(instance == null) {

            instance = new ChocolateBoiler();

        }

        return instance;

    }


    public void fill() {

        if(isEmpty()) {

            empty = false;

            boiled = false;

            //보일러에 우유, 초코릿을 혼합한 재료를 넣음

        }

    }

    public void drain() {

        if(!isEmpty() && isBoiled()) {

            //끓인 재료를 다음 단계로 넘김

            empty = true;

        }

    }

    public void boil() {

        if(!isEmpty() && !isBoiled()) {

            //재료 끓임

            boiled = true;

        }

    }


    private boolean isEmpty() {

        return empty;

    }

    private boolean isBoiled() {

        return boiled;

    }

}

싱글턴으로 변경


이슈발생
스레드가 추가된경우 (멀티스레드경우)
getInstance() 메서드에 작업이 처리되는 순서와 instance의 값이 겹칠 수 있는 경우가 발생

해결방법은
synchronized 동기화를 시켜준다.

    public static synchronized ChocolateBoiler getInstance() {
        if(instance == null) {
            instance = new ChocolateBoiler();
        }
        return instance;
    }

동기화가 꼭 필요한 시점은 이 메서드가 시작되는 때 뿐이다.
instance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메서드를 동기화된 상태로 유지시킬 필요가 없다.
첫번째 과정을 제외하면 동기화는 불필요한 오버헤드만 증가시킴.

효율적인 방법

1. getInstance()의 속도가 그리 중요하지 않다면 그냥 둔다.
=> 동기화화면 성능이 100배정도 저하된다.
/**
 * 1. 기본으로 사용하는 싱글톤 패턴
 * 2. private 생성자로 생성자를 만들수 없다. 내부에서만 가능
 * 3. intance를 체크하여 없다면 new 생성자로 만든다.
 */
public class Singleton {
    private Singleton() { }

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

2. 인스턴스를 필요할 때 생성하지 말고 처음부터 아예 만들어 버린다.
=> 클래스가 로딩될때 JVM에서 Singleton의 유일한 인스턴스를 생성해 준다. JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 instance 정적 변수에 접근할 수 없다.
/**
 * 1. 멀티스레드로 바로 instance로 바로 생성한다.
 */
public class Singleton1 {
    private Singleton1() { }

    private static Singleton1 instance = new Singleton1();
    public static Singleton1 getInstance() {
        return instance;
    }
}

3. DCL(Double-checking Locking)을 써서 getInstance에서 동기화되는 부분을 줄인다.
=> DCL을 사용하면 일단 인스턴스가 생성되어 있는지 확인 한 다음, 생성되지 않을 때만 동기화할 수 있다.
처음에만 동기화하고 나중에는 동기화를 하지 않아도 된다.
/**
 * 1. 멀티스레드로 DCL 더블클릭락킹
 * 2. volatile 멀티스레딩
 * 3. DCL 체크
 * 4. 하지만 안됨
 */
public class Singleton3 {
    private Singleton3() { }

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

4. enum으로 만든다.
=> class가 아닌 enum으로 사용
/**
 * Enum은 인스턴스가 여러 개 생기지 않도록 확실하게 보장해주고 복잡한 직렬화나 리플렉션 상황에서도 직렬화가 자동으로 지원된다는 이점이 있다.
 * 하지만 Enum에도 한계는 있다.
 * 싱글톤이 Enum 외으 클래스를 상속해야한다면 사용할 수 없다.
*/
public enum Singleton5 {
    INSTANCE;
}

5. SingletonHolder를 사용한다.
=> 내부 스태틱 클래스를 사용하여 처리
/**
 * LazyHolder
 * 1. final은 한번 초기화 되면 값을 변경할 수 없다.
 * 2. static은 Class가 로딩되는(즉, Access되는) 시점에 해당 객체가 JVM의 Class Area에 저장된다.
 * 3. Inner class를 사용함으로서 SingletonHolder.instance 이 부분이 실행되기 전까지는 2.의 법칙에 의해서 Load 되지 않는다.
 */
public class Singleton4 {
    private Singleton4() { }

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

핵심정리
- 어떤 클래스에 싱글톤 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가 최대 한개까지만 있도록 할 수 있다.
- 싱글톤 패턴을 사용하면 유일한 인스턴스를 어디서든지 접근 할 수 있도록 할 수 있다.
- 싱글톤 패턴을 구현할 때는 private 생성자와, 정적메서드, 정적변수를 사용한다.
- 다중 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용한다.
(사실 모든 애플리케이션에서 멀티스레딩을 쓸 수 있다고 생각해야 한다)
- 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고 여러 개의 인스턴스가 생길 수 있다.


출처 - Head First Design Patterns 저자- 에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠


반응형

'JAVA > Design Patterns' 카테고리의 다른 글

7.어댑터 패턴 / 퍼사드 패턴  (0) 2018.12.10
6.커맨드 패턴  (0) 2018.12.06
4. 팩토리 메서드 패턴 / 추상 팩토리 패턴  (0) 2018.12.05
2.옵저버 패턴  (0) 2018.12.04
디자인의 원칙  (0) 2018.11.29