ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴 with TS] 싱글턴 (Singleton)
    강의노트/디자인패턴 with TS 2025. 7. 7. 16:20

     

     

    1. 자바스크립트 모듈도 싱글턴

    싱글턴은 몰랐더라도, ESModule을 사용하는 웹 개발자였다면 이미 사용하고 있는 패턴이다. 다른 파일을 import를 통해 가져오는 구문에서 사용되고 되고 있는데, JS 모듈은 기본적으로 싱글턴(singleton)이다. 그렇기 때문에 아래 코드에서와 같이 다른 이름으로 import를 해왔더라도 g1과 g2는 같다. 또한 grimpan.js 파일이 서로 다른 위치에 있더라도 g1과 g2는 싱글턴으로 같다.

    // index.ts
    import g1 from './grimpan.js';
    import g2 from './grimpan.js';
    
    console.log(g1 === g2); // true
    // grimpan.ts
    class Grimpan {
      constructor(canvas: HTMLElement | null) {
        if (!canvas || !(canvas instanceof HTMLCanvasElement)) {
          throw new Error('canvas 엘리먼트를 입력해주세요');
        }
      }
    
      initialize() {}
      initializeMenu() {}
    }
    
    export default new Grimpan(document.querySelector('#canvas'));

     

    2. 타입스크립트로 구현한 싱글턴

    언어를 타지 않도록 타입스크립트로 싱글턴을 구현해본다.

    // index.ts
    import Grimpan from './grimpan.js';
    
    console.log(Grimpan.getInstance() === Grimpan.getInstance()); // true
    // grimpan.ts
    class Grimpan {
      private static instance: Grimpan;
      private constructor(canvas: HTMLElement | null) {
        if (!canvas || !(canvas instanceof HTMLCanvasElement)) {
          throw new Error('canvas 엘리먼트를 입력해주세요');
        }
      }
    
      initialize() {}
      initializeMenu() {}
    
      static getInstance() {
        if (!this.instance) {
          this.instance = new Grimpan(document.querySelector('canvas'));
        }
        return this.instance;
      }
    }
    
    export default Grimpan;

     

    싱글턴의 특징

    (1) '객체가 하나만 생성되어야 한다' 라는 제약 조건이 있을 때 사용한다.

    (2) 외부(클라이언트)에서 접근이 가능해야 한다.

     

    * 디자인 패턴에서 클라이언트는 코드를 가져다 쓰는 곳 또는 사람이다.

     

    싱글턴의 장단점

    (1) 장점

    • 객체가 하나만 생성됨을 보장할 수 있다.

    (2) 단점

    • Private 속성인 instance와 메서드(constructor)로 인해 단위 테스트에 한계가 많다.
      • getInstance를 통해서 new Grimpan을 호출하여 간접적으로  테스트 가능하나 제한적임
    • getInstance 메서드가 SOLID 원칙 중 단일책임원칙 (Single responsebility) 를 위배한다.
      • 어떤 함수/메서드/클래스는 하나의 책임을 가져야 한다. (== 코드 변경의 이유가 하나 뿐이다.)
    • 싱글턴 경우 강결합 되어 있는 경우가 많다. (→ 테스트 하기 어렵다.)

    getInstance 메서드가 단일책임원칙을 위배하는 이유

    👉 코드 변경의 이유가 하나 이상이다.

    • Grimpan을 생성할 때 다르게 생성하고 싶을 때 변경된다.
    • 유일한 인스턴스 보장을 2개까지 보장하도록 수정할 경우 변경된다.

    3. 자바스크립트(JS)로 구현한 싱글턴

    JS는 없어진 Private 타입을 멤버 변수 앞에 샵(#)을 붙여 해결할 수 있지만, constuctor를 private으로 만들어주는 것은 어렵다. 이러한 경우 꼼수로 심볼(simbol)을 활용할 수 있다. 심볼은 고유한 값이기 때문에 보통 JS의 객체 키로 사용한다. 그래서 JS에서는 심볼을 만들어서 넣어줘서 방식이 가장 깔끔하다.

    const GRIMPAN_CONSTRUCTOR_SYMBOL = Symbol();
    
    class Grimpan {
      static instance;
      constructor(canvas, symbol) {
        if (symbol !== GRIMPAN_CONSTRUCTOR_SYMBOL) {
          throw new Error('new를 통해 호출할 수 없습니다.');
        }
        if (!canvas || !(canvas instanceof HTMLCanvasElement)) {
          throw new Error('canvas 엘리먼트를 입력해주세요');
        }
      }
      
      initialize() {}
      initializeMenu() 
      
      static getInstance() {
        if (!this.instance) {
          this.instance = new Grimpan(document.querySelector('canvas',  GRIMPAN_CONSTRUCTOR_SYMBOL));
        }
        return this.instance;
      }
    }
    
    export default Grimpan;

    4. 생각해볼 문제

    • 문제: canvans1과 canvas2가 존재할 때, (즉 index.html에 canvas 엘리먼트 요소를 하나 더 있을 경우) 각 canvas 태그 당 딱 하나의 그림판 객체가 만들어지도록 코드를 개선하기
    • 힌트: 상황에 따라 변한 책임이 변한 getInstance를 수정하면 된다.
    <!DOCTYPE html>
    <html lang="ko">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>그림판</title>
      <style>
        #canvas {
          border: 1px solid black;
        }
    
        .active {
          border: 2px solid black;
        }
      </style>
    </head>
    
    <body>
      <div id="menu">
    
      </div>
      <canvas id="canvas1" width="300" height="300"></canvas>
      <canvas id="canvas2" width="300" height="300"></canvas>
      <script type="module" src="./dist/index.js"></script>
    </body>
    
    </html>

     

    grimpan.ts

    canvas 태그별로 유일한 그림판 인스턴스를 갖도록 기능을 개선한다. 기존의 싱글턴 패턴을 확장해서, Map 객체를 사용해 canvas 엘리먼트를 키(key)로, Grimpan 인스턴스를 값으로 저장하는 방식으로 구현한다. 이렇게 하면 getInstance 메서드에 어떤 canvas 엘리먼트를 전달하느냐에 따라 고유한 인스턴스를 생성하거나 반환할 수 있다.

    // canvas 태그당 하나의 그림판 객체 생성
    class Grimpan {
      // canvas 엘리먼트와 Grimpan 인스턴스를 맵 형태로 저장합니다.
      private static instance: Map<HTMLCanvasElement, Grimpan> = new Map();
      /**
       * 생성자는 private으로 선언하여 외부에서 직접 인스턴스를 생성하는 것을 방지합니다.
       * @param canvas - 이 인스턴스가 관리할 HTML Canvas 엘리먼트입니다.
       */
      private constructor(canvas: HTMLElement) {
        console.log('새로운 Grimpan 인스턴스를 생성했습니다.', canvas);
      }
    
      initialize() {}
      initializeMenu() {}
    
      static getInstance(canvas: HTMLElement | null): Grimpan {
        // 1. 전달받은 엘리먼트가 유효한 canvas 엘리먼트인지 확인합니다.
        if (!canvas || !(canvas instanceof HTMLCanvasElement)) {
          throw new Error('유효한 canvas 엘리먼트를 전달해야 합니다.');
        }
    
        // 2. Map에 해당 canvas에 대한 인스턴스가 있는지 확인합니다.
        if (!this.instance.has(canvas)) {
          // 3. 인스턴스가 없다면 새로 생성하여 Map에 저장합니다.
          const newInstance = new Grimpan(canvas);
          this.instance.set(canvas, newInstance);
        }
    
        // 4. Map에 저장된 인스턴스를 반환합니다.
        // '!'는 canvas가 Map에 반드시 존재함을 단언합니다.
        return this.instance.get(canvas)!;
      }
    }
    
    export default Grimpan;

     

    index.ts

    이제 getInstance 메서드를 호출할 때 인자로 canvas 엘리먼트를 넘겨주어야 한다. 예를 들어 HTML에 canvas1과 canvas2라는 id를 가진 두 개의 캔버스가 있다면, 아래와 같이 각각의 유일한 인스턴스를 얻을 수 있다.

    import Grimpan from './grimpan.js';
    
    const canvas1 = document.getElementById('canvas1');
    const canvas2 = document.getElementById('canvas2');
    
    const grimpanInstance1 = Grimpan.getInstance(canvas1);
    const grimpanInstance2 = Grimpan.getInstance(canvas2);
    
    // grimpanInstance1과 grimpanInstance2는 서로 다른 인스턴스입니다.
    console.log(grimpanInstance1 === grimpanInstance2); // false
    
    // 같은 canvas 엘리먼트로 다시 호출하면 이전에 생성된 인스턴스가 반환됩니다.
    const sameAsInstance1 = Grimpan.getInstance(canvas1);
    console.log(grimpanInstance1 === sameAsInstance1); // true

     

     

    출처: TS/JS 디자인 패턴 with Canvas

     

    TS/JS 디자인 패턴 with Canvas: 제로초에게 제대로 배우기 강의 | 제로초(조현영) - 인프런

    제로초(조현영) | 타입스크립트/자바스크립트로 그림판을 만들어보며 다양한 디자인 패턴의 쓰임과 장단점을 알아봅니다. canvas api를 배울 수 있는 것은 보너스!, 디자인 패턴 배워서 저한테 도

    www.inflearn.com

     

Designed by Tistory.