싱글톤 (Singleton Pattern)

자바스크립트(or 타입스크립트)로 싱글톤 패턴을 구현해 본다.

싱글톤 패턴

싱글톤 패턴이란 클래스의 인스턴스화를 하나의 객체로 제한하는 패턴이다. 싱글톤 패턴이 유용한지 아닌지에 대한 논란은 계속 있어왔으니, 다른 글을 참고하면 좋을 거 같다. 일단 어떻게 만들면 좋을지 간단하게 만들어 보자.

Object literal

이 자체로는 유용하진 않지만 객체 리터럴로 간단하게 만들 수 있다.

var Person = {
  name: 'jacob',
  age: 30,
  greeting: function () {
    return `Hi! ${this.name}`;
  },
};

유용하지 않은 이유는 이 코드가 불변성을 제공하지 않기 때문에 언제든지 재정의될 수 있기 때문이다. 조금만 더 개선해 보자.

const Person = {
  name: 'jacob',
  age: 30,
  greeting: function () {
    return `Hi! ${this.name}`;
  },
}

Object.freeze(person);
export default person;

const로 변수 선언을 하였기 때문에 재할당 할 수 없으며, Object.freeze를 통해 메서드나 속성을 변경/추가할 수 없게 만들었다(만약 기존 속성을 변경하게 해줄 목적이라면 Object.seal를 사용해도 되지만, 직접 변경의 방법을 열어주는 것보단 setter를 제공해 주는 것이 더 좋다).

대부분 객체 리터럴로 만들면 읽기 쉽고 간결하다. 하지만 과거부터 사용해 오던 전통적인 객체 지향의 구조화된 방법들을 적용할 수가 없는 단점이 있다. 일반적으로 객체 지향에서 많이 사용하는 class로는 조금 있다가 수정해 보자.

아, 물론 Object.assign()을 통해 객체 리터럴을 복사하여 사용한다면 불변성을 깨지며, Object.freeze() 메서드의 경우 **"얕은 동결"**이기 때문에 인스턴스의 속성 자체가 object 값을 가지고 있다면 불변성은 깨지게 된다.

이런 경우를 전부 대응하고 나열한다면 글이 길어질거 같아, 일단 싱글톤 패턴의 구현이라는 것에만 초점을 두고 계속 이야기를 해보자.

IIFE + Closure (모듈 패턴)

즉시 실행 함수와 클로저를 통해서도 싱글톤과 유사하게 구현할 수 있다. 별건 아니지만 타입스크립트도 추가해봤다.

const Person = (function () {
  let instance: IPerson | undefined;

  function createInstance(): IPerson {
    return {
      name: 'jacob',
      age: 30,
      greeting: function () {
        return `Hi! ${this.name}`;
      },
    };
  }

  return {
    getInstance: function () {
      if (instance === undefined) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const jacob1 = Person.getInstance();
const jacob2 = Person.getInstance();

console.log(jacob1 === jacob2); // true

사용하는 모습을 봤을 때는 전통적으로 사용하던 싱글톤의 사용방법과 유사하다. 타입만 제거한다면 자바스크립트에 class가 도입되기 전에 많이 사용했던 방식이다. 마지막으로 class로 한번 만들어 보자.

class로 구현하기 1

처음에 사용했던 Object.freeze 메서드와 class를 조합해도 구현 가능하긴 하다. 하지만 앞써 말했던 문제점이 있어 길게 설명하진 않겠다.

Person.ts
class Person {
  name = 'jacob';
  age = 30;

  greeting() {
    return `Hi! ${this.name}`;
  }
}

const instance = new Person();

Object.freeze(instance);

export default instance;

class로 구현하기 2

이번에는class로 만들때 private 접근 제어자와 static키워드를 사용해 보자.

class Person {
  private static instanceRef: Person;
  private name = 'jacob';
  private age = 30;

  private constructor() {} // new 연산자 막기

  static getInstance(): Person {
    if (Person.instanceRef === undefined) {
      Person.instanceRef = new Person();
    }
    return Person.instanceRef;
  }

  greeting() {
    return `Hi! ${this.name}`;
  }
  // or you can use getter
  getName() {
    return this.name;
  }

  // or you can use setter
  setName(newName: string) {
    this.name = newName;
  }
}

// const jacob = new Person(); // Error!

const jacob1 = Person.getInstance();
const jacob2 = Person.getInstance();

console.log({ jacob1: jacob1.getName() }); // {jacob1: "jacob"}
console.log({ jacob2: jacob2.getName() }); // {jacob2: "jacob"}

jacob1.setName('cob');

console.log({ jacob1: jacob1.getName() }); // {jacob1: "cob"}
console.log({ jacob2: jacob2.getName() }); // {jacob2: "cob"}

위의 코드를 리뷰하자면, private 클래스 생성자를 지정하여 new 키워드로 인스턴스를 여러 개 만들 수 없도록 하였고, static 키워드로 클래스 메서드를 정적 메서드로 만들어, 특정 인스턴스가 아닌 클래스에만 속하도록 만들었다.

getInstance() 메서드는 정적 메서드이므로 클래스 인스턴스가 없을 때 메서드를 호출하며, instanceRef에 저장된 단일 인스턴스의 참조가 없는 경우에만 새 인스턴스를 만들고 그렇지 않은 경우에는 저장된 인스턴스 참조를 반환한다.

마지막으로

싱글톤 패턴은 싱글톤 객체를 호출할 때마다 기존에 만들어진 인스턴스가 있다면 새 인스턴스를 생성하지 않고 기존 객체에 대한 참조를 반환한다. 즉 전역 범위에서 런타임 중 싱글톤은 한 번만 생성되게 된다.

싱글톤의 장점은 단일 접근 지점을 제공하여, 전역 네임 스페이스에서 구연 코드를 분리하는 공유 네임 스페이스 역할을 하며, 단일 인스턴스에서 모든 정보의 흐름을 중앙화 하여 관리할 수가 있다. 데이터 베이스 연결이나 전역 상태 관리 등에서 유용하게 사용될 수 있다.

싱글톤의 단점은 너무 많은 책임을 가지거나 많은 데이터를 공유하게 되면, 클래스간 결합도가 너무 높아져 OCP를 위반하게 된다. 따라서 수정과 테스트가 어려워 지게 된다. 또한 싱글톤은 자신의 인스턴스가 있는지 확인하고 유지되도록 하기 때문에 두 가지를 담당하여 SRP를 위반한다.

어떤 디자인 패턴이건 마찮가지겠지만, 항상 좋은 점만 있는 패턴은 없다. 적절한 순간에 적절한 패턴을 사용할 수 있는 개발자가 되자.

Last updated