nowwater 2023. 4. 18. 23:29
728x90

디자인 패턴 중 생성 패턴

  • 팩토리 메소드
  • 추상 팩토리
  • 빌더
  • 싱글턴
  • 프로토타입

싱글턴 패턴

클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근(액세스) 지점을 제공하는 생성 디자인 패턴

생성자를 private 으로 지정하여 인스턴스 생성을 불가능하게 한 후, getInstance() 라는 정적 메서드를 구현하여 어디서든 호출 가능하게 하여 인스턴스를 관리한다.

용도: 연결 풀, 스레드 풀과 같은 자원 풀 관리, 캐시, 대화상자, 사용자 설정, 레지스트리 설정 처리, 로그 기록용 객체, 디바이스 드라이버 등

 

또한 추상팩토리 패턴을 싱글턴으로 구현 가능하다.

 

클라이언트들은 항상 같은 객체와 작업하고 있다는 사실을 인식조차 못 할 수 있다.

 

싱글턴 대신 전역 변수를 사용한다면

  • 애플리케이션이 끝날 때까지 그 객체를 한 번도 안 쓸 경우 괜히 자원만 잡아먹음
  • 싱글턴의 게으른 생성(Lazy Instantiation)을 사용할 수 없음

 

싱글턴 패턴의 문제점

1. 단일 책임 원칙 위배

  • 자신의 인스턴스를 관리하는 일 외에도 원래 그 인스턴스를 사용하고자 하는 목적에 부합하는 작업을 책임져야 함

2. 클래스 로더가 여러 개라면 같은 클래스를 여러 번 로딩할 수도 있음

    → 클래스 로더를 직접 지정해주면 해결 가능

 

3. 리플렉션, 직렬화, 역직렬화 시 문제가 될 수 있음

     → enum 으로 싱글턴을 생성해서 해결 가능

 

4. 느슨한 결합 원칙 위배

  • 싱글턴 객체에 의존하는 객체는 전부 하나의 객체에 단단하게 결합됨
  • 싱글턴을 바꾸면 연결된 모든 객체를 바꿔야 할 가능성

5. 멀티 스레드 환경에서 여러 스레드가 싱글턴 객체를 여러 번 생성하지 않도록 특별한 처리가 필요

 

6. 서브 클래스 생성 불가능

  • private 생성자 → 확장 불가능
  • 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존하기 때문에, 싱글턴 클라이언트 코드의 단위 테스트를 하기 어려움

싱글턴은 애초에 특수한 상황에서 제한된 용도로 사용하려고 만들어졌기 때문에, 싱글턴을 많이 사용했다면 전반적인 디자인을 다시 한 번 생각해봐야 함

 

싱글턴의 단점

  • 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존하기 때문에, 싱글턴 클라이언트 코드의 단위 테스트를 하기 어려움
  • 대부분 언어에서 정적 메서드를 오버라이딩하는 것이 불가능
    • 싱글턴의 한계를 극복할 수 있는 창의적인 방법을 생각해야함
    • 아니면 그냥 테스트를 작성하지 말거나 싱글턴 패턴을 사용하지 않으면 됨

 

역직렬화, 리플렉션 시 문제 발생

역직렬화

  • 역직렬화 자체가 보이지 않은 생성자로서 역할을 수행하기 때문에 인스턴스를 또 다시 만들어, 직렬화에 사용한 인스턴스와는 전혀 다른 인스턴스를 생성
  • 해결방법: readResolve 메서드 사용
    • readResolve 메서드를 정의하게 되면, 역직렬화 과정에서 readObject를 통해 만들어진 인스턴스 대신 readResolve에서 반환되는 인스턴스를 내가 원하는 것으로 바꿀 수 있음
    • 기존에 역직렬화를 통해 생성된 객체는 자연스럽게 가비지 컬렉션 대상이 됨
  • 싱글턴 객체 내 필드 변수들이 있을 경우, 실제로 직렬화 시에는 readResolve 로 바꿔치기 하기 때문에 실 데이터가 필요 없어 모든 인스턴스 필드를 transient로 선언해주는 것이 좋다.
    • 역직렬화 과정 중간에 역직렬화된 인스턴스의 참조를 훔쳐오는 공격을 행할 경우 다른 객체로 바뀔 위험이 있기 때문
class Singleton implements Serializable {

    // 싱글톤 객체의 필드들을 transient 설정하여 직렬화 제외
    transient String str = "";
    transient ArrayList lists = new ArrayList();
    transient Integer[] integers;

    private Singleton() {}

    private static class SettingsHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SettingsHolder.INSTANCE;
    }

    // 역직렬화한 객체는 무시하고 클래스 초기화 때 만들어진 인스턴스를 반환
    private Object readResolve() {
        return SettingsHolder.INSTANCE;
    }
}

 

리플렉션

  • 클래스 객체를 통해 해당 객체의 생성자를 받아와 newInstance() 메서드를 실행하면 인스턴스를 생성할 수 있게 되는데, 여기서 생성된 인스턴스는 Holder가 가지고 있는 인스턴스와는 전혀 다른 새로운 인스턴스이기 때문
public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    /* Reflection API */

    // 1. Singleton의 Class에서 생성자를 가져온다
    Constructor<Singleton> consructor = Singleton.class.getDeclaredConstructor();

    // 2. 생성자가 private 이기 때문에 외부에서 access 할 수 있도록 true 설정
    consructor.setAccessible(true);

    // 3. 가져온 생성자를 이용해 인스턴스화 한다
    Singleton singleton1 = consructor.newInstance();
    Singleton singleton2 = consructor.newInstance();

    System.out.println("singleton1 == singleton2 : " + (singleton1 == singleton2));
}

// 실행 결과
// singleton1 == singleton2 : false

 

직렬화 & 역직렬화는 readResolve() 를 구현함으로써 대응할 수 있었지만, 리플렉션은 대응 불가

 

Enum 으로 해결 가능

  • 애초에 멤버를 만들때 private로 만들고 한번만 초기화 하기 때문에 Thread-Safe 하며, enum 내에서 상수 뿐만 아니라 변수나 메서드를 선언해 사용이 가능하기 때문에, 이를 이용해 독립된 싱글톤 클래스 처럼 응용이 가능
  • 열거형은 리플렉션을 통해 newInstance() 를 실행하지 못하도록 막아 놓았기 때문에 애초에 리플렉션 동작이 불가능
  • Enum은 기본적으로 serializable 인터페이스를 구현하고 있기 때문에 직렬화도 역시 가능
enum Singleton {
    INSTANCE; // 싱글톤 인스턴스

    // 이넘은 필드와 메서드도 가질 수 있다
    private int value = 3;

    public int getValue() {
        return value;
    }
}

public static void main(String[] args) {
    Singleton singleton1 = Singleton.INSTANCE;
    Singleton singleton2 = Singleton.INSTANCE;

    System.out.println("singleton1 == singleton2 : " + (singleton1 == singleton2));
    System.out.println(singleton1.getValue());
}

// 실행 결과
singleton1 == singleton2 : true
3
public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {

    /* Reflection API */

    // 1. Singleton Enum의 생성자는 숨겨져 있기 때문에 getDeclaredConstructors로 배열로 가져온다.
    Constructor<?>[] consructors = Singleton.class.getDeclaredConstructors();

    // 2. 생성자 배열을 순회하여 인스턴스를 생성한다
    for(Constructor<?> constructor : consructors){
        constructor.setAccessible(true); // 생성자가 private 이기 때문에 외부에서 access 할 수 있도록 true 설정
        Singleton singleton = (Singleton) constructor.newInstance("INSTANCE");
    }
}

 

 

Enum 싱글턴의 단점

싱글턴으로 구성할 클래스가 특정 클래스의 상속이 필요한 구성일 경우, enum은 같은 enum 이 외의 클래스 상속은 불가능하기 때문에 어쩔수 없이 일반적인 클래스로 구성해야 한다.