도찐개찐

[코드디자인패턴-JAVA] Singleton Pattern 본문

프로그래밍/코드디자인패턴

[코드디자인패턴-JAVA] Singleton Pattern

도개진 2024. 2. 29. 09:28

Singleton Pattern(싱글톤 패턴)

사용시점:

  1. 단일 인스턴스 필요시: 로그 파일 작성기, 드라이버 객체, 캐시, 스레드 풀 등과 같이 전체 시스템에서 단 하나의 인스턴스만 유지 되어야 하는 경우에 사용
  2. 자원 공유: 여러 클라이언트가 동일한 리소스에 엑세스 해야 하는 경우에 싱글톤 패턴이 유용
  3. 비용 절감(재사용): 객체를 생성하는 데 비용이 많이 드는 경우, 싱글톤 패턴을 사용해 한번만 생성하고 재사용

장점:

  1. 인스턴스 제어: 인스턴스가 한 개만 생성 되는 것을 보장하므로, 중복 생성으로 인한 리소스 낭비 방지
  2. 전역 엑세스 포인트 제공: 싱글톤 객체는 애플리케이션 내에서 쉽게 엑세스 할 수 있는 전역 엑세스 포인트를 제공
  3. 자원 공유: 동일한 객체를 여러 클라이언트와 공유 할 수 있으므로, 자원을 효율적으로 사용

단점:

  1. 멀티스레드 이슈: 멀티스레드 환경에서 동시에 여러개의 인스턴스가 생성 될 수 있습니다. 이를 방지하기 위한 동기화 메커니즘이 필요하며 이는 성능 저하 가능성이 있음
  2. 결합도 증가: 전역 인스턴스로써, 다른 클래스들과 과도하게 결합될 가능성이 있습니다. 이로 인해 코드 유지보수와 테스트가 어려워 질 수 있습니다.
  3. 스케일링 이슈: 인스턴스가 한 개만 존재해야 하므로, 확장성 떨어집니다. 따라서 싱글톤 패턴을 적절하게 사용하지 않으면 소프트웨어 시스템 전체에 부정적인 영향을 끼칠 수 있습니다.

결론

  • 이런 장단점을 잘 고려하여 싱글톤 패턴을 적절한 상황에 사용하는 것이 중요합니다. 특히 멀티스레드 환경에서의 안전성과 객체 간의 느슨한 결합을 유지하는 것이 중요합니다.
  1. Eager Initialization(즉시초기화):
  • 장점
    1. 클래스가 로딩 될 때 인스턴스를 생성하는 방법
    2. 생성비용이 낮고 멀티 쓰레드 환경에서 안전
    3. 클래스 로딩 시점에 인스턴스가 생성되므로, 불필요 리소스 낭비
  • 단점
    1. 리소스 낭비: Eager Initialization는 프로그램 시작 시 모든 리소스를 초기화합니다. 그런데 이들 리소스 중 실제로 사용되지 않는 것들이 있다면, 그 초기화는 시간과 메모리 자원의 낭비가 됩니다.
    2. 시작 지연: 초기화에 시간이 많이 걸리는 리소스가 있다면, 그 리소스의 Eager Initialization은 프로그램 시작을 지연시키는 원인이 될 수 있습니다.
    3. 초기화 순서 문제: 객체 간의 종속성이 있을 때, 초기화 순서를 정확히 관리해야 합니다. 초기화 순서가 잘못되면 런타임 에러를 발생시킬 수 있습니다.
    • Lazy Initialization이 이런 문제를 해결할 수 있지만,
    • 그 자체의 문제점(동시성 문제, 복잡성 증가 등)도 있습니다.
    • 따라서 적절한 초기화 전략을 선택하는 것이 중요합니다.
    • 경우에 따라서는 두 방식을 적절히 혼합하는 것이 최선의 방법일 수도 있습니다.
public class EagerInitializationSingleton {
    private static final EagerInitializationSingleton instance = 
        new EagerInitializationSingleton();

    private EagerInitializationSingleton() {}

    public static EagerInitializationSingleton getInstance() { return instance; }

    public String hello() { return "hello, Eager Initialization"; }
}

// ------------ Main ----------
public class EagerMain {
    public static void main(String[] args) {
        EagerInitializationSingleton eager = EagerInitializationSingleton.getInstance();

        System.out.println(eager.hello());
    }
}
EagerMain.main(null);
hello, Eager Initialization
  1. LazyInitialization(지연초기화)
  • 장점
    1. 인스턴스를 필요한 시점에 생성 함으로 애플리케이션 시작시에 리소스를 사용하지 않음.
  • 단점
    1. 멀티스레드 환경에서의 문제: 여러 스레드가 동시에 초기화를 시도하면, 한 번에 하나의 인스턴스만 생성되어야 하는 것이 두 개 이상 생성될 수 있습니다. 동기화(synchronization)를 이용해 해결 할 수 있지만, 추가적인 오버헤드 발생 가능성이 있습니다.
    2. 성능 저하: 객체가 처음 요청될 때까지 초기화를 연기하면, 해당 객체가 실제로 필요할 때까지 애플리케이션 성능이 저하될 수 있습니다. 초기화에 많은 시간이 걸리는 경우, 사용자가 해당 데이터나 서비스를 기다리는 시간이 늘어나게 됩니다.
    3. 초기화 순서 문제: 객체 간의 종속성이 있는 경우, 적절한 초기화 순서를 보장하는 것이 어려울 수 있습니다. Lazy Initialization이 이를 복잡하게 만들 수 있습니다.
    4. 이런 문제를 피하기 위해, 멀티스레드 환경에서는 동기화 메커니즘을 사용해야 하며, 종속성이 있는 경우는 초기화 순서를 적절히 관리해야 합니다. 또한 초기화 시간이 긴 객체의 경우, 비동기 방식으로 초기화를 고려해볼 수 있습니다.
public class LazyInitializationSingleton {
    /**
     * 여기서 volatile 키워드는 instance가 여러 스레드에 의해 공유 되므로,
     * 한 스레드가 instance를 수정하면 다른 스레드가 이를 즉시 보도록 합니다.
     */
    private static volatile LazyInitializationSingleton instance;

    private LazyInitializationSingleton() {}

    public static synchronized LazyInitializationSingleton getInstance() {
        /*
        if (instance == null) {
            instance = new LazyInitializationSingleton();
        }
        */

        /**
         * Lazy Initialization에서 동기화 메커니즘을 사용하는 일반적인 방법 중 하나는 "double-checked locking" 패턴입니다.
         * 이 패턴은 인스턴스가 이미 초기화 되었는지 먼저 확인 하고(첫 번째 검사),
         * 초기화되지 않았을 경우에만 동기화 블록을 실행 합니다.
         * 동기화 블록 내에서는 다시 한번 인스턴스가 초기화 되었는지 확인합니다.(두 번째 검사)
         *
         * 그러나 double-checked locking 패턴은 복잡하고, 올바르게 사용하지 않으면 문제가 발생할 수 있습니다.
         * 따라서 대부분의 경우, 초기화를 위한 동기화를 단순화하는 것이 좋습니다.
         * 예를 들어, Java에서는 Bill Pugh Singleton 패턴이 이러한 목적에 적합합니다.
         */
        if (instance == null) {
            synchronized(LazyInitializationSingleton.class) {
                if (instance == null) {
                    instance = new LazyInitializationSingleton();
                }
            }
        }

        return instance;
    }

    public String hello() {
        return "hello, Lazy Initialization";
    }
}

public class LazyMain {
    public static void main(String[] args) {
        LazyInitializationSingleton lazy = LazyInitializationSingleton.getInstance();
        System.out.println(lazy.hello());
    }
}

LazyMain.main(null)
hello, Lazy Initialization
  1. Bill Pugh Singleton
  • 내부 정적 클래스를 사용해 싱글톤을 구현하는 방법.
  • 내부 정적 클래스가 JVM에 로드 될 때 싱글톤 인스턴스를 생성하므로, Lazy Initialization의 장점을 가지면서도 멀티스레드 환경에 안전 함
  • 단점
  1. 복잡도: 다른 싱글톤 패턴들에 비해 이해하고 구현하는 데에 약간 더 복잡할 수 있습니다. 이 패턴은 내부 정적 보조 클래스를 사용해 싱글톤 인스턴스를 생성하기 때문에 코드를 처음 보는 사람에게는 혼란스러울 수 있습니다.
  2. 리플렉션의 취약성: 리플렉션을 이용하면 private 생성자를 통해 여러개의 인스턴스를 만들 수 있습니다.
  3. 직렬화 역직렬화: 직렬화 후 역직렬화 하는 과정에서 새로운 인스턴스가 생길 수 있습니다. 이를 방지하기 위해서는 readResolve()메서드를 구현해야 합니다.
  • 결론
    • 위 단점들에도 불구하고, Bill pugh Singleton 패턴은 Lazy initialization 장점과 스레드의 안전성을 모두 충족하면서, 동기화 오버헤드 없이 싱글톤 인스턴스를 생성하는 방법으로 잘 알려져 있습니다.
    • 이러한 장점이 단점을 상쇄하는 경우가 많습니다.
public class BillPughSingleton {
    private BillPughSingleton() {}

    private static class SingletonHelper {
        private static final BillPughSingleton instance = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() { return SingletonHelper.instance; }

    public String hello() { return "hello, Bill pugh"; }

    /**
     * readResolve() 메서드는 싱글톤 인스턴스를 반환하므로,
     * 역직렬화 후에도 기존 싱글톤 인스턴스를 유지할 수 있습니다.
     * 이 방법을 통해 싱글톤 패턴이 역직렬화 과정에서도 안전하게 유지 될 수 있습니다.
     */
    protected Object readResolve() { return getInstance(); }
}

public class BillPughMain {
    public static void main(String[] args) {
        BillPughSingleton billPugh = BillPughSingleton.getInstance();

        System.out.println(billPugh.hello());
    }
}

BillPughMain.main(null);
hello, Bill pugh
  1. Enum Singleton
  • Enum을 사용해 싱글톤을 구현하는 방법
  • 자바 Enum이 쓰레드에 안전하고 한번만 로드 되는 것을 보장하기 때문에, 가장 안전하고 간단하게 싱글톤을 구현 할 수 있습니다.
  • 단점
  1. 유연성 제한: 상속을 통한 확장이 불가능 합니다. 즉, 싱글톤이 다른 클래스를 상속해야하는 경우에는 Enum Singleton을 사용할 수 없습니다.
  2. 직렬화 제한: Enum은 자바에서 직렬화가 자동으로 처리되므로, 개발자가 직렬화 과정을 세밀하게 제어할 수 없습니다.
  3. 리플렉션 공격에 대한 방어 제한: Enum은 리플렉션을 통해 공격받을 가능성이 낮지만, 완벽하게 방어할 수는 없습니다. 자바 리플렉션 API는 Enum도 공격할 수 있습니다.
  4. 코드 가독성: Enum을 사용한 싱글톤 패턴은 일반적인 클래스 기반의 싱글톤 패턴보다 이해하기 어려울 수 있습니다. 따라서 코드를 읽는 사람이 이 패턴에 익숙하지 않다면 혼란을 줄 수 있습니다.
public enum EnumSingleton {
    INSTANCE;

    public String hello() { return "hello, Enum Singleton"; }
}

public class EnumMain {
    public static void main(String[] args) {
        EnumSingleton enum1 = EnumSingleton.INSTANCE;
        System.out.println(enum1.hello());
    }
}

EnumMain.main(null)
hello, Enum Singleton
728x90
Comments