[Design Pattern] 싱글턴 패턴

olwooz·2023년 2월 12일

Design Pattern

목록 보기
6/22
클래스에 하나의 인스턴스만 있고, 그 인스턴스에 대한 전역 접근 지점을 제공하는 생성 패턴

문제

싱글턴 패턴 - 한 번에 두 가지의 문제를 해결하며 SRP를 위반함

  1. 클래스가 하나의 인스턴스만 있도록 함
    • 클래스의 인스턴스 수를 제한하는 가장 흔한 이유 → 공유 자원 접근 제어를 위해 (e.g. DB/파일)
    • 객체를 하나 생성하고 또 객체를 생성하면 새로운 객체를 생성하는 대신 이미 존재하는 객체를 받음
    • 일반 생성자 호출은 특성상 반드시 새 객체를 반환해야 하므로 일반 생성자로 구현 불가
  2. 해당 인스턴스에 대한 전역 접근 지점 제공
    • 필수 객체들을 저장하는 전역 변수는 편리하지만 변수의 내용을 덮어쓸 수 있어 안전하지 않음
    • 싱글턴 패턴은 전역 변수처럼 특정 객체를 프로그램 어디서나 접근할 수 있게 해주지만, 다른 코드에 의해 덮어쓰이는 것도 방지해줌
    • 코드의 나머지 부분이 이미 문제 1을 해결하는 코드에 의존하고 있다면, 문제 1을 해결하는 코드가 프로그램 전체에 흩어져 있는 것보다 한 클래스 안에 두는 것이 더 좋음

최근에는 싱글턴 패턴이 대중화돼서 위의 두 문제 중 하나만 해결해도 싱글톤이라고 부를 수 있음

해결책

모든 싱글턴의 구현은 공통적으로 아래 두 단계를 거침:

  • 기본 생성자를 private으로 만들어 다른 객체들이 싱글턴 클래스에 new 연산자 쓰는 것 방지
  • 생성자 역할을 하는 정적 생성 메서드 생성, 내부적으로 이 메서드는 private 생성자를 호출해 객체를 만들고 정적 필드에 저장하고, 이 메서드에 대한 이어지는 호출들은 캐시된 객체 반환

코드가 싱글턴 클래스에 접근할 수 있으면 싱글톤 정적 메서드 호출 가능 → 해당 메서드가 호출될 때마다 항상 같은 객체 반환

구조

  1. 싱글턴 클래스 - 해당 클래스의 같은 인스턴스를 반환하는 정적 메서드 getInstance 선언
    • 싱글턴의 생성자는 클라이언트 코드로부터 숨겨져야 하고, getInstance 메서드를 호출하는 게 싱글턴 객체를 받아오는 유일한 방법이여야 함

적용

클래스가 하나의 인스턴스만으로 모든 클라이언트에게 제공되어야 하는 경우

- e.g. 프로그램의 여러 부분에서 공유되는 하나의 데이터베이스 객체
- 싱글턴 패턴은 특별한 생성 메서드를 제외한 클래스의 모든 객체 생성 방법을 비활성화함 
  → 생성 메서드는 새 객체를 생성하거나, 이미 생성되었다면 기존 객체를 반환

전역 변수에 보다 엄격한 제어가 필요한 경우

- 전역 변수와 다르게 싱글턴 패턴은 클래스에 하나의 인스턴스만 존재함을 보장, 
  싱글턴 클래스 자체를 제외하고 그 무엇도 캐시된 인스턴스를 대체할 수 없음
- `getInstance` 본문을 변경해서 싱글턴 인스턴스의 최대 갯수를 임의로 조정할 수 있음

구현방법

1. 싱글턴 인스턴스를 저장할 클래스에 private 정적 필드 추가

2. 싱글턴 인스턴스를 받아오는 public 정적 생성 메서드 선언

3. 정적 메서드 내부에 “지연된 초기화” 구현 
   → 첫 호출에는 새로운 객체를 만들어 정적 필드에 넣고, 이후 호출에는 항상 그 인스턴스를 반환
   
4. 클래스의 생성자를 private으로 만듦 → 클래스의 정적 메서드는 생성자를 호출할 수 있지만 다른 객체들은 불가능

5. 클라이언트 코드에서 싱글턴 생성자에 대한 직접 호출을 정적 생성 메서드로 변경

장단점

장점

- 클래스의 단일 인스턴스 보장
- 해당 인스턴스에 대한 전역 접근 지점 획득
- 싱글턴 객체를 처음 요청받았을 때에만 초기화됨

단점

- SRP 위반 - 두 문제를 한 번에 해결함
- 나쁜 설계를 감출 수 있음 (e.g. 프로그램의 컴포넌트들이 서로에 대해 너무 많이 알고 있는 경우)
- 멀티스레드 환경에서 여러 개의 스레드가 싱글턴 객체를 여러 번 생성하지 않도록 특별 조치 필요
- 많은 테스트 프레임워크는 mock 객체를 생성할 때 상속에 의존하기 때문에 클라이언트 코드의 유닛 테스트가 어려움
    - 싱글턴 클래스의 생성자는 private이고 정적 메서드를 override하는 건 대부분 언어에서 불가능하기 때문에 
      싱글턴을 mock할 창의적인 방법 사용 or 테스트 작성 X or 싱글톤 패턴 사용 X

다른 패턴과의 관계

- 퍼사드 패턴은 대부분 하나의 퍼사드 객체면 충분하기 때문에 싱글턴 패턴으로 변경 가능

- 객체들의 모든 공통 상태를 하나의 플라이웨이트 객체로 줄이면 플라이웨이트 패턴과 싱글톤 패턴이 비슷해지만 
  두 가지 근본적인 차이점 존재
    1. 싱글턴 인스턴스는 하나만 있어야 하지만 
       플라이웨이트 클래스는 고유 상태가 다른 여러 개의 인스턴스를 가질 수 있음
    2. 싱글턴 객체는 변경 가능하지만 플라이웨이트 객체는 변경 불가
    
- 추상 팩토리, 빌더, 프로토타입 패턴 모두 싱글턴 패턴으로 구현 가능

TypeScript 예제

/**
 * The Singleton class defines the `getInstance` method that lets clients access
 * the unique singleton instance.
 */
class Singleton {
    private static instance: Singleton;

    /**
     * The Singleton's constructor should always be private to prevent direct
     * construction calls with the `new` operator.
     */
    private constructor() { }

    /**
     * The static method that controls the access to the singleton instance.
     *
     * This implementation let you subclass the Singleton class while keeping
     * just one instance of each subclass around.
     */
    public static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }

        return Singleton.instance;
    }

    /**
     * Finally, any singleton should define some business logic, which can be
     * executed on its instance.
     */
    public someBusinessLogic() {
        // ...
    }
}

/**
 * The client code.
 */
function clientCode() {
    const s1 = Singleton.getInstance();
    const s2 = Singleton.getInstance();

    if (s1 === s2) {
        console.log('Singleton works, both variables contain the same instance.');
    } else {
        console.log('Singleton failed, variables contain different instances.');
    }
}

clientCode();
// Output.txt

Singleton works, both variables contain the same instance.

참고 자료: Refactoring.guru

0개의 댓글