ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디자인 패턴 스터디 기록 (4) - 데코레이터 패턴
    ✏️ 스터디 모음집/디자인 패턴 스터디 2022. 11. 2.

    데코레이터 패턴

    코드를 직접 수정하지 않고 코드를 확장하는 기법.

    주로 특정한 로직에 대한 전처리, 후처리가 계속해서 반복되는 경우 이를 재사용 하는 목적으로 사용된다.

    예를 들어 백엔드에서 REST API를 작성하려고 할때, HTTP Method를 엔드포인트 별로 다르게 설정해주고 싶다면, 매번 엔드포인트를 작성 할 때마다 어떠한 HTTP Method로 요청 되었는 지 확인하는 로직이 필요할 것이다. 이런 경우 메소드 데코레이터를 사용하여 전처리 과정을 재사용 할 수 있다.

    from django.views.decorators.http import require_http_methods
    
    @require_http_methods(["GET", "POST"]) # HERE!
    def my_view(request):
        # I can assume now that only GET or POST requests make it this far
        # ...
        pass
    

    Django의 경우 위와 같은 데코레이터가 내장되어 있어서, 앞에서 설명한 상황을 간단히 그리고 가독성 좋게 해결 할 수 있다.

     

    데코레이터 패턴의 구현

    책에서는 객체지향 프로그래밍의 관점에서 설명하고 있기에 가장 먼저 공통된 개념을 담고 있는 인터페이스 또는 추상 클래스를 선언하고, 인터페이스를 상속받는 구상 클래스와 이를 장식하게 될 데코레이터를 작성한다.

    이를 음료 대신에 칵테일, 자바 대신에 타입스크립트로 간단하게 작성 해보았다

    export abstract class Cocktail {
      protected description: string;
      protected cost: number;
    
      constructor() {
        this.description = '그냥 물입니다';
        this.cost = 0;
      }
    
      public abstract getCost(): number;
      public abstract getDesc(): string;
    }

    가장 먼저 추상 메소드를 담은 추상 클래스인 Cocktail을 선언한다.

    import { Cocktail } from "./Cocktail";
    
    export class GinTonic extends Cocktail {
      constructor() {
        super();
        this.cost = 5000;
        this.description = '진&토닉';
      }
    
      public getCost(): number {
        return this.cost;
      }
      public getDesc(): string {
        return this.description;
      }
    }

    그 다음 이를 상속받는 칵테일의 구현인 GinTonic을 작성한다

    import { Cocktail } from "./Cocktail";
    
    export abstract class CondimentDecorator extends Cocktail {
      cocktail: Cocktail;
    
      constructor(cocktail: Cocktail) {
        super();
        this.cocktail = cocktail;
      }
    
      public abstract getDesc(): string;
      public abstract getCost(): number;
    }
    

    그리고 이를 장식할 데코레이터가 상속 받을 클래스인 CondimentDecorator를 선언하고

    import { Cocktail } from "./Cocktail";
    import { CondimentDecorator } from "./CondimentDecorator";
    
    export class LemonSlice extends CondimentDecorator {
      constructor(cocktail: Cocktail) {
        super(cocktail);
      }
    
      public getCost(): number {
        return this.cocktail.getCost() + 500; 
      }
    
      public getDesc(): string {
        return '레몬 슬라이스로 장식한 ' + this.cocktail.getDesc();
      }
    }

    데코레이터의 구현 부분인 LemonSlice를 작성했다.

    let ginTonic: Cocktail = new GinTonic();
    ginTonic = new LemonSlice(ginTonic);
    
    console.log(ginTonic.getDesc(), `: ${ginTonic.getCost()}원`); 
    // 레몬 슬라이스로 장식한 진&토닉 : 5500원

     

    위와 같이 진토닉의 인스턴스를 생성하고 이를 LemonSlice로 장식하면 레몬 슬라이스로 장식한 진토닉이 완성된다.

     

    설탕이 들어간 데코레이터

    일부의 데코레이터를 사용 할 수 있는 ( 함수가 일급 객체인 ) 언어는 데코레이터를 사용하기 쉽도록 문법적 설탕이 포함되어 있다. 가장 처음 첨부 했던 django의 코드 또한 이러한 문법 설탕으로 포현 되었으며, JS에도 이러한 구문이 있다. 아직 es 사양에 정식으로 포함되지 않았기에 babel에 기대 사용할 수 있다. ( typescipt를 사용한다면 tsconfig에서 experimentalDecorators 설정을 켜주어야한다 )

    구현은 다음과 같다

    function withGarnish(name: string, price: number) {
      return function (constructor: typeof Cocktail) {
        const getCost = constructor.prototype.getCost;
        const getDesc = constructor.prototype.getDesc;
    
        constructor.prototype.getCost = function () {
          return getCost() + price;
        }
        constructor.prototype.getDesc = function () {
          return `${name}로 장식한 ` + getDesc() ;
        }
      }
    }
    
    @withGarnish('라임 웨지', 500)
    export class GinFizz extends Cocktail {
      constructor() {
        super();
      }
    
      public getCost(): number {
        return 5000;
      }
      public getDesc(): string {
        return '진피즈';
      }
    }
    
    // 문법 설탕 스타일
    const ginFizz = new GinFizz();
    
    console.log(ginFizz.getDesc(), `: ${ginFizz.getCost()}원`);
    // 라임 웨지로 장식한 진피즈 : 5500원

     

    댓글

GitHub: https://github.com/Yesung-Han