Self-Dev/Design Patterns R&D

[17] 디자인 패턴 목록 - 생성 디자인 패턴 - 싱글턴 패턴(Singleton Pattern)

Khadra 2024. 6. 21. 08:10

출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서




싱글턴

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



문제

싱글턴 패턴은 한 번에 두 가지의 문제를 동시에 해결함으로써 단일 책임 원칙을 위반한다.

  • 1. 클래스에 인스턴스가 하나만 있도록 한다.
    • 클래스의 인스턴스를 제어하는 가장 일반적인 이유는 공유 리소스(예: 데이터베이스 또는 파일)에 대한 접근을 제어하기 위해서이다.
    • 객체를 생성하고 나면, 같은 객체를 계속 사용할 수 있다.


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

  • 2. 전역 접근 지점을 제공한다.
    • 필요한 객체에 쉽게 접근할 수 있지만, 이로 인해 다른 코드가 객체를 덮어쓸 수 있어 충돌 가능성이 있다.
    • 싱글턴 패턴은 이러한 문제를 방지할 수 있다.
    • 코드를 모듈화하여 문제 해결 코드를 한 곳에 집중시킬 수 있다.

최근에는 싱글턴 패턴이 일부 문제만 해결해도 사용될 수 있습니다.



해결책

싱글턴 구현은 일반적으로 다음 두 가지 단계로 이루어진다.

  • 클래스의 생성자를 비공개로 설정하여 외부에서 인스턴스를 생성할 수 없게 한다.
  • 정적 메서드를 사용하여 객체를 생성하고 이를 저장한 후, 해당 객체를 반환하도록 한다.
    • 이렇게 하면 항상 동일한 객체가 반환된다.


실제상황 적용

정부는 싱글턴 패턴의 좋은 예입니다.
국가는 하나의 공식 정부만 가질 수 있으며, 'X의 정부'라는 용어는 해당 정부의 책임자들을 식별하는 글로벌 접근 지점이다.



구조

  • 싱글턴 클래스는 getInstance라는 정적 메서드를 선언하여 자체 클래스의 동일 인스턴스를 반환한다.
    • 이 메서드를 통해 클라이언트는 싱글턴 객체에 접근할 수 있다.


의사코드

  • 해당 예시는 데이터베이스 연결 클래스는 싱글턴 역할을 한다.
  • 공개 생성자가 없으므로 getInstance 메서드를 통해 객체를 가져올 수 있다.
    • 이 메서드는 초기에 객체를 생성하고 이후 호출에서는 캐시된 객체를 반환한다.
‘// 데이터베이스 클래스는 클라이언트들이 프로그램 전체에서 데이터베이스 연결의 같은
// 인스턴스에 접근할 수 있도록 해주는 `getInstance`(인스턴스 가져오기) 메서드를
// 정의합니다.
class Database is
  // 싱글턴 인스턴스를 저장하기 위한 필드는 정적으로 선언되어야 합니다.
  private static field instance: Database

  // 싱글턴의 생성자는 `new` 연산자를 사용한 직접 생성 호출들을 방지하기 위해
  // 항상 비공개여야 합니다.
  private constructor Database() is
    // 데이터베이스 서버에 대한 실제 연결과 같은 일부 초기화 코드.

  // 싱글턴 인스턴스로의 접근을 제어하는 정적 메서드.
  public static method getInstance() is
    if (Database.instance == null) then
      acquireThreadLock() and then
        // 이 스레드가 잠금 해제를 기다리는 동안 인스턴스가 다른
        // 스레드에 의해 초기화되지 않았는지 확인하세요.
        if (Database.instance == null) then
          Database.instance = new Database()
    return Database.instance

  // 마지막으로 모든 싱글턴은 해당 로직의 인스턴스에서 실행할 수 있는 
  // 비즈니스 로직을 정의해야 합니다.
  public method query(sql) is
    // 예를 들어 앱의 모든 데이터베이스 쿼리들은 이 메서드를 거칩니다. 
    // 따라서 여기에 스로틀링 또는 캐싱 논리를 배치할 수 있습니다.
    // …

class Application is
  method main() is
    Database foo = Database.getInstance()
    foo.query("SELECT ...")
    // …
    Database bar = Database.getInstance()
    bar.query("SELECT ...")
    // 변수 `bar`는 변수 `foo`와 같은 객체를 포함할 것입니다.


적용

  • 싱글턴 패턴은 프로그램의 클래스에서 클라이언트가 공유해야 하는 단일 인스턴스만 필요할 때 사용된다.

    • 예를 들어 데이터베이스 객체와 같은 경우이다.
      • 이 패턴은 클래스의 다른 인스턴스 생성 방법을 비활성화하고, getInstance 메서드를 통해 항상 동일한 인스턴스를 반환한다.
  • 전역 변수를 엄격히 제어하고 싶을 때 싱글턴 패턴을 사용할 수 있다.

    • 싱글턴 패턴은 전역 변수와 달리 오직 하나의 인스턴스만을 보장한다.
    • 이 인스턴스는 프로그램 전체에서 유일무이하게 사용된다.


구현 방법

  • 1. 클래스에 비공개 정적 필드를 추가하여 싱글턴 인스턴스를 저장하세요.


  • 2. 공개된 정적 생성 메서드를 선언하여 싱글턴 인스턴스를 가져오세요.


  • *3. 정적 메서드 내에서 '지연된 초기화'를 구현하세요. *

  • 이 방식은 첫 호출 시 객체를 생성하고 이후 호출에서는 캐시된 객체를 반환한다.


  • 4. 클래스의 생성자를 비공개로 만들어 직접적인 인스턴스화를 막으세요.


  • 5. 클라이언트 코드에서 직접 생성자 호출을 getInstance 메서드 호출로 대체하세요.



장단점

장점

  • 클래스가 하나의 인스턴스만을 갖는다는 점을 확신할 수 있다.
  • 전역 접근 지점을 제공하여 어디서든 해당 인스턴스에 접근할 수 있다.
  • 싱글턴 객체는 처음 요청될 때만 초기화되므로 자원 절약 효과를 가져올 수 있다.

단점

  • 단일 책임 원칙을 위반할 가능성이 있다.
    • 한 클래스가 여러 역할을 동시에 수행하려고 할 때 발생할 수 있습니다.
  • 싱글턴 패턴은 의도하지 않은 디자인을 가리킬 수 있다.
    • 컴포넌트들이 서로 강하게 의존하는 상황을 만들 수 있습니다.
  • 다중 스레드 환경에서 싱글턴 객체를 여러 번 생성하지 않도록 특별한 처리가 필요하다.
    • 동기화 문제를 해결하지 않으면 안전하지 않을 수 있습니다.
  • 싱글턴 객체를 테스트하기 어려울 수 있다.
    • 특히, 싱글턴 클래스의 생성자가 비공개이고 정적 메서드를 오버라이드하기 어려운 경우가 있습니다.

다른 패턴과의 관계

  • 퍼사드 패턴의 클래스는 종종 싱글턴으로 변환될 수 있다.

    • 하나의 인스턴스만 필요한 경우에 퍼사드를 싱글턴으로 구현할 수 있습니다.
  • 플라이웨이트 패턴은 여러 인스턴스를 가질 수 있지만 공유 상태를 관리할 때 단일 인스턴스를 사용할 수 있는 경우가 있다.

    • 싱글턴은 항상 하나의 인스턴스만을 가지지만, 플라이웨이트는 여러 상태를 공유할 수 있는 여러 인스턴스를 포함할 수 있다.
  • 추상 팩토리, 빌더, 프로토타입 등의 패턴들도 싱글턴으로 구현할 수 있다.

    • 필요한 경우 단일 인스턴스의 생성과 관리를 위해 이러한 패턴들을 활용할 수 있습니다.