ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인패턴]싱글턴 패턴
    IT 발자취.../디자인패턴 2018. 12. 9. 03:15

    1. 정의
    애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴.

    생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 반환한다. (자바에선 생성자를 private로 선언해서 생성 불가하게 하고 getInstance()로 받아쓰기도함)

    즉, 인스턴스가 필요할 때 똑같은 인스턴스를 만들어 내는 것이 아니라, 동일(기존) 인스턴스를 사용하게함

    2. 개요
    싱글톤 패턴을 쓰는 이유
    - 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지
    - 싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스의 데이터를 공유하기 쉽다.
    - DBCP(DataBase Connection Pool)처럼 공통된 객체를 여러개 생성해서 사용해야하는 상황에서 많이 사용
    (쓰레드풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정, 로그 기록 객체 등)
    - 인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶을 경우 사용
    - 두 번째 이용시부터는 객체 로딩 시간이 현저하게 줄어 성능이 좋아지는 장점

    싱글톤패턴의 문제점
    싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 "개방-폐쇄 원칙"을 위배하게 된다. (=객체 지향 설계 원칙에 어긋남)
    따라서 수정이 어려워지고 테스트하기 어려워진다.
    또한 멀티 쓰레드 환경에서 동기화 처리를 안하면 인스턴스가 두개가 생성된다든지 하는 경우가 발생할 수 있음
    꼭 필요한 경우가 아니면 지양해야함. //적절히 사용하면 아주 좋다.

    3. 예시
    1. <기존의 방법> Lazy Instance  -> private 생성자를 이용하여 객체 생성 후 반환

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

    Lazy Instance는 멀티스레드 환경에서 문제가 생기게 됩니다. 이를 해결하기 위해 getInstance()를 동기화(Synchronized)해주면 문제를 해결할 수 있습니다.하지만 synchronized 특성상 큰 성능저하가 발생합니다. 동기화가 필요한 시점은 메소드가 시작하는 때 뿐입니다. 일단 uniqueInstance 변수에 Singleton 인스턴스 대입 후 동기화 된 상태를 유지할 필요가 없습니다.
    참고로 메소드 동기화를하면 성능이 약 100배 가량 느려집니다.

    2. Thread Safe Lazy Initialization + Double-Checking Locking
    메서드에 Synchronized를 뺘ㅐ면서 오버헤드를 줄이고자 하였습니다.

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

    getInstance()에 Synchronized를 사용하는 것이 아니라 첫 번째 if문으로 인스턴스의 존재여부를 체크하고 두 번째 if문에서 다시 한번 체크할 때 동기화 시켜서 인스턴스를 생성하므로 thread-safe하면서도 처음 생성 이후에 synchonized블럭을 타지 않기 때문에 성능 저하를 완화하였다. 
    하지만 현재 broken Idiom이고 사용 권고하지 않고 있다.

    3. Enum 사용
    Enum은 인스턴스가 여러개 생기지 않도록 확실하게 보장해주고 복잡한 직렬화나 리플렉션 상황에서도 직렬화가 자동으로 지원된다. 그리고 multi thread로 부터 안전하다.

    public enum Singleton { INSTANCE; public void test() { System.out.println("singleton by enum"); } }

    Enum의 한계도 있다 -> Android 개발시 보통 Singleton의 초기화 과정에서 Context라는 의존성이 끼어들 가능성이 높다.

    4. Lazy Holder 방식 (현재 제일 추천)
    객체가 필요할 때로 초기화를 미룹니다. 이 방식은 jvm 상에 클래스 초기화 단계에 따른 특성을 활용한 방식입니다.

    public class LazyHolder { private LazyHolder() {} private static class Singleton { private static final LazyHolder INSTANCE = new LazyHolder(); } public static LazyHolder getInstance() { return Singleton.INSTANCE; } public void test() { System.out.println("singleton by LazyHolder"); } }

    LazyHolder 클래스는 클래스 로드 시점에 초기화되지만 정적 클래스로 정의된 내부 클래스의 초기화는 해당 시점에 이뤄지지 않는 특성이 있습니다. 즉, getInstace를 통해 내부 클래스의 instance를 호출할 때까지 초기화를 미루는 것입니다. 
    이 방식은 thread safe한 이유는 jvm의 클래스 초기화 과정에서 보장되는 원자적 특성(시리얼하게 동작)을 이용하기 때문입니다. 즉, 동기화 문제를 jvm이 처리하도록 합니다.

    [참고 : 클래스가 최초로 초기화 되는 시점]
    T에서 선언한 정적 필드가 상수 변수가 아니며 사용되었을 때
    T가 클래스이며 T의 인스턴스가 생성될 때
    T가 클래스이며 T에서 선언한 정적 메소드가 호출되었을 때
    T에서 선언한 정적 필드에 값이 할당되었을 때
    T가 최상위 클래스 (상속 관계에서)이며 T안에 위치한 assert 구문이 실행 되었을 때

    직렬화 가능 클래스에서 역직렬화시 새로운 객체가 생기게 되는 문제 (Singleton 깨짐, 단 enum 싱글턴 방식은 역직렬화에 따른 문제점이 없다. lazy holder 방식은 미확인)

    싱글턴 특성을 유지하려면
    1. 모든 필드를 transient 로 선언
    2. readResolve 메서드 추가
    //싱글턴 상태를 유지하기 위한 readResolve 구현
    private Object readResolve() {
        //동일한 객체가 반환되도록 하는 동시에, 가짜 객체는 gc가 처리하도록 만듬.
        return INSTANCE;
    }

    'IT 발자취... > 디자인패턴' 카테고리의 다른 글

    [디자인패턴]팩토리 패턴  (0) 2018.12.09
    [디자인패턴] 데코레이터 패턴  (0) 2018.12.09

    댓글

Designed by Gintire