ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴 with TS] 추상 팩토리 패턴으로 객체를 세트로 생성하기
    카테고리 없음 2025. 8. 4. 21:10

     

    여러 객체가 세트로 구성되어 있을 때 사용하는 팩토리 패턴이다. 아래와 같이 그림판이 브라우저 타입에 따라 달라질 수 있는 메뉴(initializeMenu 메서드) 초기화 기능까지 정의하게 되면 단일책임원칙을 위반하게 된다. 그렇기 때문에 브라우저별 세트로 구성되도록 추상 팩토리 패턴을 적용해본다.

    // AbstractGrimpan.ts
    export default abstract class Grimpan {
      protected constructor(canvas: HTMLElement | null) {
        if (!canvas || !(canvas instanceof HTMLCanvasElement)) {
          throw new Error('canvas 엘리먼트를 입력하세요.');
        }
      }
    
      abstract initialize(): void;
      abstract initializeMenu(): void;
    
      static getInstance() {}
    }

     

    1. Menu 관련 기능 추가

    이제 GrimpanFactroy.ts 와 GrimpanMenu.ts 파일을 만들어서 관련 있는 코드를 밀집시켜 정리해본다.

     

    GrimpanFactory.ts

    팩토리와 관련된 클래스를 GrimpanFactory.ts 파일에 모으고, 메뉴와 관련된 기능을 추가한다.

    // GrimpanFactory.ts (기존 AbstractGrimpanFactory.ts)
    
    import Grimpan from './AbstractGrimpan.js';
    import ChromeGrimpan from './ChromeGrimpan.js';
    import { ChromeGrimpanMenu, IEGrimpanMenu } from './GrimpanMenu.js';
    import IEGrimpan from './IEGrimpan.js';
    
    export default abstract class AbstractGrimpanFactory {
      static createGrimpan() {
        throw new Error('하위 클래스에서 구현하셔야 합니다.');
      }
      static createGrimpanMenu(grimpan: Grimpan) {
        throw new Error('하위 클래스에서 구현하셔야 합니다.');
      }
    }
    
    export class ChromeGrimpanFactory extends AbstractGrimpanFactory {
      static override createGrimpan() {
        return ChromeGrimpan.getInstance();
      }
      static override createGrimpanMenu(grimpan: ChromeGrimpan) {
        return ChromeGrimpanMenu.getInstance(grimpan);
      }
    }
    
    export class IEGrimpanFactory extends AbstractGrimpanFactory {
      static override createGrimpan() {
        return IEGrimpan.getInstance();
      }
      static override createGrimpanMenu(grimpan: IEGrimpan) {
        return IEGrimpanMenu.getInstance(grimpan);
      }
    }

     

    GrimpanMenu.ts

    싱글턴으로 구현된 메뉴와 관련된 기능을 GrimpanMenu.ts 파일을 만들어 정리한다.

    // GrimpanMenu.ts
    
    import Grimpan from './AbstractGrimpan.js';
    import ChromeGrimpan from './ChromeGrimpan.js';
    import IEGrimpan from './IEGrimpan.js';
    
    export abstract class GrimpanMenu {
      grimpan: Grimpan;
      protected constructor(grimpan: Grimpan) {
        this.grimpan = grimpan;
      }
    
      abstract initialize(): void;
    
      static getInstance(grimpan: Grimpan) {}
    }
    
    export class IEGrimpanMenu extends GrimpanMenu {
      private static instance: IEGrimpanMenu;
      override initialize(): void {}
      static override getInstance(grimpan: IEGrimpan): IEGrimpanMenu {
        if (!this.instance) {
          this.instance = new IEGrimpanMenu(grimpan);
        }
        return this.instance;
      }
    }
    
    export class ChromeGrimpanMenu extends GrimpanMenu {
      private static instance: ChromeGrimpanMenu;
      override initialize(): void {}
      static override getInstance(grimpan: ChromeGrimpan): IEGrimpanMenu {
        if (!this.instance) {
          this.instance = new ChromeGrimpanMenu(grimpan);
        }
        return this.instance;
      }
    }

     

    2. History 관련 기능 추가

    Grimpanhistroy 기능을 추가해본다. 이때 코드를 수정하는지 초점을 맞춰서 살펴보자. 기존 코드의 수정이 안 된다면 OCP를 준수하고 있다는 의미이다. Histroy 기능을 추가하면서 기존 코드는 수정없이 추가만 이루어진다. index.ts 파일의 main 함수를 통해 Chrome과 관련된 그림판, 그림판 메뉴, 그림판 히스토리 객체가 만들어진다. 이처럼 추상 팩토리를 사용하면 관련 있는 것을 세트로 생성할 수 있다. 신기능을 추가할 때마다 기존 코드를 수정하지 않고 추가할 수 있다.

     

    indext.ts

    main 함수의 인수로 AbstarctGrimpanFactory 타입을 받는다면 더 편하겠지만, 타입스크립트는  abstract 키워드를 static 속성과 함께 사용하는 것을 지원하지 않아 에러가 발생한다. 근본적으로 abstract는 인스턴스 레벨의 구현을 강제하는 반면, static은 클래스 레벨에서 직접 존재하기 때문에 둘은 함께 쓰일 수 없다.

    • abstract는 "나는 상속하는 인스턴스는 이걸 꼭 만들어야 해!"라고 말한다.
    • static은 "클래스 자체에 존재하는 거야."라고 말한다.

    따라서 abstract static 이라는 조합은 "클래스 자체에 존재하면서, 동시에 미래에 파생될 인스턴스가 반드시 구현해야 하는"이라는 모순적인 의미가 되어 버린다. 타입스크립트의 설계는 이러한 모순을 허용하지 않는다.

    • abstract와 static의 역할
      • abstract (추상): abstract로 선언된 멤버(메서드나 속성)는 인스턴스(instance)가 구현해야 할 설계도를 정의한다. 따라서 abstract 멤버는 클래스 자체에 존재하는 것이 아니라, 해당 클래스로부터 생성된 각 인스턴스의 구체적인 구현을 위한 약속이다.
      • static (정적): static으로 선언된 멤버는 클래스 자체이에 속하며, 인스턴스를 생성하지 않고도 접근할 수 있다. new 키워드로 객체를 만들지 않아도 '클래스이름.정적멤버' 와 같이 바로 사용할 수 있다. 이는 모든 인스턴스가 공유하는 단 하나의 멤버이며, 인스턴스의 구체적인 구현과는 관련이 없다.
    // index.ts
    import { ChromeGrimpanFactory } from './GrimpanFactory.js';
    
    function main() {
      const factory = ChromeGrimpanFactory;
      const grimpan = factory.createGrimpan();
      const grimpanMenu = factory.createGrimpanMenu(grimpan);
      const grimpanHistory = factory.createGrimpanHistory(grimpan);
      grimpan.initialize();
      grimpanMenu.initialize();
      grimpanHistory.initialize();
    }
    
    main();

     

    GrimpanFactory.ts

    // GrimpanFactory.ts
    
    import Grimpan from './AbstractGrimpan.js';
    import ChromeGrimpan from './ChromeGrimpan.js';
    import { ChromeGrimpanHistory, IEGrimpanHistory } from './GrimpanHistory.js';
    import { ChromeGrimpanMenu, IEGrimpanMenu } from './GrimpanMenu.js';
    import IEGrimpan from './IEGrimpan.js';
    
    export default abstract class AbstractGrimpanFactory {
      static createGrimpan() {
        throw new Error('하위 클래스에서 구현하셔야 합니다.');
        // 리스코프 치환 원칙을 준수하는 예시 코드
        // return Grimpan.getInstance() as unknown as Grimpan;
      }
      static createGrimpanMenu(grimpan: Grimpan) {
        throw new Error('하위 클래스에서 구현하셔야 합니다.');
      }
      static createGrimpanHistory(grimpan: Grimpan) {
        throw new Error('하위 클래스에서 구현하셔야 합니다.');
      }
    }
    
    export class ChromeGrimpanFactory extends AbstractGrimpanFactory {
      static override createGrimpan() {
        return ChromeGrimpan.getInstance();
      }
      static override createGrimpanMenu(grimpan: ChromeGrimpan) {
        return ChromeGrimpanMenu.getInstance(grimpan);
      }
      static override createGrimpanHistory(grimpan: ChromeGrimpan) {
        return ChromeGrimpanHistory.getInstance(grimpan);
      }
    }
    
    export class IEGrimpanFactory extends AbstractGrimpanFactory {
      static override createGrimpan() {
        return IEGrimpan.getInstance();
      }
      static override createGrimpanMenu(grimpan: IEGrimpan) {
        return IEGrimpanMenu.getInstance(grimpan);
      }
      static override createGrimpanHistory(grimpan: IEGrimpan) {
        return IEGrimpanHistory.getInstance(grimpan);
      }
    }

     

    GrimpanHistory.ts

    // GrimpanHistory.ts
    
    import Grimpan from './AbstractGrimpan.js';
    import ChromeGrimpan from './ChromeGrimpan.js';
    import IEGrimpan from './IEGrimpan.js';
    
    export abstract class GrimpanHistory {
      grimpan: Grimpan;
      protected constructor(grimpan: Grimpan) {
        this.grimpan = grimpan;
      }
    
      abstract initialize(): void;
    
      static getInstance(grimpan: Grimpan) {}
    }
    
    export class IEGrimpanHistory extends GrimpanHistory {
      private static instance: IEGrimpanHistory;
      override initialize(): void {}
      static override getInstance(grimpan: IEGrimpan): IEGrimpanHistory {
        if (!this.instance) {
          this.instance = new IEGrimpanHistory(grimpan);
        }
        return this.instance;
      }
    }
    
    export class ChromeGrimpanHistory extends GrimpanHistory {
      private static instance: ChromeGrimpanHistory;
      override initialize(): void {}
      static override getInstance(grimpan: ChromeGrimpan): IEGrimpanHistory {
        if (!this.instance) {
          this.instance = new ChromeGrimpanHistory(grimpan);
        }
        return this.instance;
      }
    }

     

     

     

     

     

     

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

     

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

    현재 평점 5점 수강생 518명인 강의를 만나보세요. 타입스크립트/자바스크립트로 그림판을 만들어보며 다양한 디자인 패턴의 쓰임과 장단점을 알아봅니다. canvas api를 배울 수 있는 것은 보너스!

    www.inflearn.com

     

Designed by Tistory.