Ayden's journal

싱글턴 패턴

싱글턴 패턴(Singleton Pattern)은 소프트웨어 디자인 패턴 중 하나로, 애플리케이션 내에서 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 패턴이다. 주로 전역 상태를 관리하거나 공통된 자원을 효율적으로 활용해야 하는 경우에 사용된다. 이 패턴은 인스턴스의 생성과 접근을 중앙에서 제어함으로써 불필요한 메모리 낭비를 방지하고, 일관된 상태를 유지할 수 있는 장점을 가진다. 일반적으로 클래스 내부에 자신의 인스턴스를 정적(static)으로 보관하고, 외부에서는 해당 인스턴스에 접근할 수 있는 메서드(예: getInstance())를 통해 객체를 반환하는 방식으로 구현한다. 이러한 특성 덕분에 싱글턴 패턴은 설정 관리, 로깅, 캐시 처리, 데이터베이스 연결 등 다양한 분야에서 널리 활용된다.

이번 포스트에서는 모 회사의 코딩 과제 도중에 스태틱 메서드로 작성했던 DialogStore 클래스를 싱글턴 클래스로 바꾸어보며 싱글턴 패턴의 특징을 알아보려 한다.

class DialogStore {
  private static _store: Store;
  private static callbacks: listener[] = [];

  static get store() {
    return this._store;
  }

  static set store(value: Store) {
    if (value !== undefined && this._store !== undefined) return;

    this._store = value;
    this.callbacks.forEach((cb) => cb());
  }

  static addListener(listener: listener) {
    this.callbacks.push(listener);
  }

  static removeListener(listener: listener) {
    this.callbacks = this.callbacks.filter((l) => l !== listener);
  }
}

 

Lazy Initialization

명시적으로 생성되는 싱글턴은 객체의 인스턴스를 클래스 외부에서 명시적으로 생성하는 방식이다. 일반적으로 클래스 내부에서 인스턴스를 생성하는 메서드가 제공되며, 이를 통해 인스턴스가 한 번만 생성되도록 보장한다. 이 방식에서는 인스턴스를 직접 요청하거나, getInstance()와 같은 메서드를 통해 명시적으로 인스턴스를 생성하게 된다. 명시적으로 생성된 싱글턴은 인스턴스의 생성 및 관리를 개발자가 직접 제어할 수 있어 유연하게 활용할 수 있지만, 인스턴스 생성 시점과 방법을 명확히 설정해야 하므로 관리가 다소 복잡할 수 있다.

private constructor()는 싱글턴 패턴에서 인스턴스가 외부에서 직접 생성되지 않도록 막는 역할을 한다. 즉, 외부에서 new 키워드를 사용하여 인스턴스를 생성할 수 없게 하여, 싱글턴 인스턴스가 하나만 존재하도록 보장하는 핵심적인 부분이다.

export class DialogStore {
  private static _instance: DialogStore;
  private _store: Store;
  private callbacks: listener[] = [];
  
  // private 생성자
  private constructor() {}

  // 싱글턴 인스턴스 반환
  static get instance(): DialogStore {
    if (!this._instance) {
      this._instance = new DialogStore();
    }
    return this._instance;
  }

  // store getter
  get store() {
    return this._store;
  }

  // store setter
  set store(value: Store) {
    if (value !== undefined && this._store !== undefined) return;
    this._store = value;
    this.callbacks.forEach((cb) => cb());
  }

  // 리스너 추가
  addListener(listener: listener) {
    this.callbacks.push(listener);
  }

  // 리스너 제거
  removeListener(listener: listener) {
    this.callbacks = this.callbacks.filter((l) => l !== listener);
  }
}

 

Eager Initialization

자동으로 관리되는 싱글턴은 객체의 인스턴스를 자동으로 생성하는 방식으로, 주로 클래스 내부에서 인스턴스를 생성하고 관리한다. 이 방식에서는 클래스가 최초로 참조될 때 인스턴스를 자동으로 생성하고, 이후 동일한 인스턴스를 반환하여 외부에서의 인스턴스 생성을 차단한다. 개발자는 객체를 생성하거나 직접 제어할 필요 없이, 단순히 자동으로 관리되는 인스턴스를 사용하는 것만으로 싱글턴 패턴을 구현할 수 있다. 이 방식은 코드가 간결하고 관리가 용이하지만, 인스턴스 생성 시점에 대한 제어가 부족할 수 있어 유연성이 떨어질 수 있다.

export class DialogStore {
  private static _instance = new DialogStore(); // 인스턴스를 자동으로 생성
  private _store: Store;
  private callbacks: listener[] = [];

  // private 생성자
  private constructor() {}

  // 싱글턴 인스턴스 반환
  static get instance(): DialogStore {
    return this._instance;
  }

  // store getter
  get store() {
    return this._store;
  }

  // store setter
  set store(value: Store) {
    if (value !== undefined && this._store !== undefined) return;
    this._store = value;
    this.callbacks.forEach((cb) => cb());
  }

  // 리스너 추가
  addListener(listener: listener) {
    this.callbacks.push(listener);
  }

  // 리스너 제거
  removeListener(listener: listener) {
    this.callbacks = this.callbacks.filter((l) => l !== listener);
  }
}

 

코드 가독성 면에서 클래스 간의 의존성이 확실히 드러나는 것은 매우 중요하다. 생성자를 통해 선언된 클래스 간의 의존성, 매개변수 전달 같은 경우 함수의 정의를 보면 쉽게 식별할 수 있다. 그러나 싱글턴 클래스는 명시적으로 생성할 필요가 없고 매개변수 전달에 의존할 필요도 없으며 함수에서 직접 호출할 수 있음에도 의존성이 전혀 드러나지 않는다. 따라서 코드를 읽을 때 각 함수의 코드 구현 자체를 주의 깊게 살펴봐야만 이 클래스가 어떤 싱글턴 클래스에 의존하는지 알 수 있다.

 

 

 

추가적으로. 싱글턴 패턴의 변형으로 멀티턴 패턴(Multiton Pattern) 혹은 다중 인스턴스 패턴(Multi Instance Pattern)이라는 게 있다. 여러 개의 인스턴스를 하나의 클래스에서 관리하지만, 각 인스턴스가 특정 키에 의해 구별되는 방식이다. 즉, 클래스의 각 키마다 유일한 인스턴스를 생성하여 저장하고, 동일한 키로 요청할 경우 동일한 인스턴스를 반환한다. 이를 통해 여러 키에 대해 각각 고유한 인스턴스를 가질 수 있으며, 이를 통해 자원을 효율적으로 관리할 수 있다. 

이러한 멀티턴 패턴은 상태 관리나 캐시 시스템에서도 효과적으로 활용될 수 있다. 예를 들어, 애플리케이션이 여러 사용자 세션을 관리해야 할 경우, 각 사용자 ID를 키로 하여 고유한 세션 인스턴스를 유지할 수 있다. 이로 인해 불필요한 인스턴스 생성을 방지하고, 동일한 키에 대해 항상 일관된 상태를 유지할 수 있는 장점이 있다. 또한, 싱글턴과 유사하게 글로벌 액세스가 가능하면서도, 필요한 만큼의 인스턴스를 효율적으로 관리할 수 있다는 점에서 유연성이 높다. 그러나 너무 많은 인스턴스를 관리하게 되면 메모리 사용량이 증가할 수 있으므로, 적절한 자원 관리가 필요하다.

class Logger {
  private static instances: Map<string, Logger> = new Map();
  private static allowedFiles = ['log1.txt', 'log2.txt', 'log3.txt'];

  // private 생성자로 외부에서 직접 인스턴스 생성 방지
  private constructor(private fileName: string) {}

  // Multiton 인스턴스 반환
  static get instance(fileName: string): Logger {
    if (!this.allowedFiles.includes(fileName)) {
      throw new Error(`허용되지 않은 파일입니다: ${fileName}`);
    }

    if (!this.instances.has(fileName)) {
      this.instances.set(fileName, new Logger(fileName));
    }

    return this.instances.get(fileName)!;
  }

  // 로그 추가
  public log(message: string) { ... }

  // 현재 저장된 로그 확인 (테스트용)
  public getLogs() { ... }
}

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기