치춘짱베리굿나이스

자바스크립트에서의 싱글톤 패턴과 static 본문

Javascript + Typescript/이론과 문법

자바스크립트에서의 싱글톤 패턴과 static

치춘 2022. 8. 8. 15:29

자바스크립트에서의 싱글톤 패턴과 static

싱글톤의 의미

Singleton

이름의 ‘Single’ 에서 뭔가 한 개..? 하나의…? 라는 뜻이 연상된다

싱글톤은 특정 클래스에 인스턴스를 단 하나만 생성 (메모리를 단 한 번만 할당) 하여 사용하는 패턴을 의미한다 (= 메모리를 단 한 번만 할당한다)

특징

  • 인스턴스가 딱 하나만 생성된다 (대개 private, static 등의 키워드를 이용하여 구현한다)
  • ‘단 한 개’ 만 생성되는 인스턴스는 전역으로 참조될 수 있으며, 다른 객체들이 이 공유된 인스턴스에 접근할 수 있어 데이터 공유가 편리하다
  • 메모리를 한 번만 할당하면 되기 때문에 효율이 좋다

인스턴스가 ‘단 하나' 임을 보장하고 싶을 때 주로 사용한다

단점

  • 일반 클래스보다 더 복잡하며, 비동기적인 상황에서 동시에 인스턴스가 생성될 수 있는 문제 (동시성) 를 해결할 수 있어야 한다
  • 테스트가 어렵다
    • 전역으로 단 하나만 존재하는 인스턴스이기 때문에, 내부의 속성값이 변하지 않아 테스트를 할 때마다 인스턴스를 초기화해 주어야 한다
  • 자식 클래스를 만들 수 없다
  • 싱글톤 인스턴스가 너무 많은 업무를 하거나, 외부 객체에 데이터 공유가 잦아질 경우 인스턴스들 간의 결합도가 높아진다
    • 이는 ‘개방-폐쇄 원칙’을 위반한다
    • 개방 폐쇄 원칙이란: 기존 코드를 변경하지 않으면서 (Close) 기능을 추가 (Open) 할 수 있어야 함을 의미한다
    • 객체간 결합도가 높아질 수록, 기존의 의존성 때문에 기능 추가가 어려워질 수밖에 없다

자바스크립트에서 싱글톤 구현해보기

클래스 선언하기

class Singleton {
  static #instance;

  constructor(name) {
    if (Singleton.#instance) {
      console.log("Instance already available!!");
      return Singleton.#instance;
    }
    this.name = name;
    Singleton.#instance = this;
  }

  printName() {
    console.log(this.name);
  }
}

export default Singleton;

정말정말 간단한 클래스를 만들어 보았다

눈에 띄는 부분은 static 키워드인데, 이 키워드를 사용하면 정적 메서드 (정적 프로퍼티) 로써, 인스턴스가 아닌 클래스 자체에 메서드 또는 프로퍼티를 설정해줄 수 있다

 

test() {
  console.log(this.#instance);
}

해당 값을 읽어들이는 메서드를 시도해봤지만,

정적 메서드는 개별 객체가 아닌 클래스에 속한 함수이므로, this 키워드로 읽어들일 수 없다

 

test() {
  console.log(Singleton.#instance);
}

이렇게 클래스 함수의 내부 함수로 호출하면 읽어들일 수 있다

지금은 딱 하나의 인스턴스를 생성한 상태이기 때문에, 해당 인스턴스가 노출되고 있다

 

console.log(Singleton.#instance);

더불어, private한 프로퍼티로 지정했기 때문에 클래스 내부 (생성자, 메서드 등) 가 아닌 다른 곳에서는 읽어들일 수 없다

 

static #instance;

constructor(name) {
  if (Singleton.#instance) {
    console.log("Instance already available!!");
    return Singleton.#instance;
  }
  this.name = name;
  Singleton.#instance = this;
}

이처럼 static 키워드를 이용하여 클래스 그 자체에 인스턴스 프로퍼티를 지정하면, 인스턴스가 이미 존재하는지 여부를 생성자에서 판단하는 코드를 작성할 수 있다

  1. 클래스 내부의 private 프로퍼티 (앞에 #을 붙인다) 로 #instance를 선언하였다
    • 아직 값은 할당하지 않고, 클래스 함수가 선언될 시점에 선언만 같이 해준다
    • 값 초기화는 첫 번째 인스턴스가 생성되는 시점 (처음으로 생성자가 호출되는 시점) 에 이루어진다
  2. 생성자는 인자로 name을 받는다
  3. Singleton.#instance에 값이 이미 존재한다면, 생성자가 호출된 적이 있고 해당 클래스로 생성된 인스턴스가 존재한다는 의미이므로 Singleton.#instance에 저장된 인스턴스 참조만을 반환한다
    • console.log는 테스트용으로 출력해 보았다
  4. Singleton.#instance에 값이 존재하지 않는다면 인스턴스가 생성된 적 없다는 의미이므로, 그 아랫줄로 내려가 인스턴스를 생성한다
    • 인스턴스의 name 프로퍼티는 인자로 받은 name을 사용한다
  5. 첫 번째 인스턴스가 생성되었다! Singleton.#instance에 해당 인스턴스의 참조 (this) 를 넣어 초기화해 준다

인스턴스 만들어보기

const singleton1 = new Singleton("babo");
const singleton2 = new Singleton("javascript");
singleton1.printName();
singleton2.printName();

babo 인스턴스와 javascript 인스턴스를 생성하고, printName 메서드를 통해 이름을 출력해 보자

 

뜨헉

분명 babo 인스턴스와 javascript 인스턴스를 생성해서 둘 다 이름을 출력해 보았는데 어째 이름은 babo만 출력되고 있다

singleton1 인스턴스가 이미 만들어졌으므로, singleton2 인스턴스는 추가로 생성되지 않고 대신 singleton1 인스턴스의 참조를 반환하기 때문이다

javascript라는 이름은 아무 의미 없는 값이 되었다

생성자 private하게 만들기

class Singleton {
  static #instance;
  static #isFromInit;

  constructor(name) {
    if (!Singleton.#isFromInit)
      throw "ConstructorError: Cannot construct new instance from constructor. use Init() instead.";
    if (Singleton.#instance) return Singleton.#instance;
    this.name = name;
    Singleton.#instance = this;
    Singleton.#isFromInit = false;
  }

  static init(name) {
    Singleton.#isFromInit = true;
    return new Singleton(name);
  }
}

export default Singleton;

static 키워드를 응용하여 생성자 함수를 호출하지 않고 외부 함수를 통해야만 인스턴스가 생성되도록 만들 수 있다

  1. static하고 private한 프로퍼티인 isFromInit을 선언한다
    • 이 값은 boolean으로, 후술할 init 함수를 통해서 생성자가 호출되었는지 여부를 검사한다
  2. 생성자 함수의 최상단에 Singleton.#isFromInit 프로퍼티의 값을 검사하는 조건문을 추가한다
    • init 함수를 통해 생성자가 호출되었으면 이 값이 true일 것이고, 아니면 false 또는 undefined일 것이다
    • 따라서 이 값이 true일 때만 새로운 인스턴스를 반환할 수 있도록 설정해준 것이다
    • init 함수를 통해서 호출되지 않았을 경우, 예외를 throw하여 인스턴스가 생성되지 않도록 한다
    • 인스턴스를 생성하고 나서, #isFromInitfalse로 초기화해준다
  3. init 함수를 구성한다
    • Singleton.#isFromInittrue로 바꿔주어 init 함수로부터 생성자가 호출되었음을 알린다
    • 생성자를 호출하고 그 인스턴스를 바로 반환한다

 

const singleton1 = new Singleton("babo");

이렇게 생성자를 바로 거칠 경우 예외가 발생한다

 

const singleton1 = Singleton.init("babo");
const singleton2 = Singleton.init("javascript");
singleton1.printName();
singleton2.printName();

비단 싱글턴 객체를 만들 때뿐만 아니라, 생성자에 함부로 접근하지 않도록 제한을 주고 싶을 때 응용하면 좋다

기타

자바스크립트도 나름 키워드 구현이 많이 되어있어 간단하게 싱글톤 클래스를 구현할 수 있었다

어떻게 활용할지는? 앞으로 해나가야 할 과제인 듯 하다


참고 자료

[디자인 패턴] 싱글톤 패턴(Singleton Pattern) 정리 및 예제 - 생성 패턴

싱글톤(Singleton) 패턴이란?

싱글톤 패턴(Singleton pattern)을 쓰는 이유와 문제점

[Javascript] Singleton Pattern 싱글톤 패턴

Class fields - JavaScript | MDN

How to define private constructors in javascript?

Comments