JavaScript 데코레이터 구현

succeeding·2022년 10월 21일
0

데코레이터 패턴을 JavaScript에서 구현한 예시 코드

Concrete Component

class StackCalculator {
  constructor () {
    this.stack = []
  }

  putValue(value) {
    this.stack.push(value)
  }

  getValue() {
    return this.stack.pop()
  }

  peekValue() {
    return this.stack[this.stack.length - 1]
  }

  clear() {
    this.stack = []
  }

  divide() {
    const divisor = this.getValue()
    const dividend = this.getValue()
    const result = dividend / divisor
    this.putValue(result)
    return result
  }

  multiply() {
    const multiplicand = this.getValue()
    const multiplier = this.getValue()
    const result = multiplier * multiplicand
    this.putValue(result)
    return result
  }
}

이제 위 StackCalculatoradd 동작을 추가하고, divide에 validation을 추가하는 데코레이터를 세 가지 방법으로 구현하겠다.

구현

Composition

class EnhancedCalculator {
  constructor(calculator) {
    this.calculator = calculator
  }

  // 새로운 함수
  add() {
    const addend2 = this.getValue()
    const addend1 = this.getValue()

    const result = addend1 + addend2
    this.putValue(result)
    return result
  }

  // 수정된 함수
  divide() {
    // 추가적인 검증 로직
    const divisor = this.calculator.peekValue()
    if (divisor === 0) {
      throw Error('Division by 0')
    }
    // Subject에 대한 유효한 위임자(delegates)일 경우
    return this.calculator.divide()
  }

  // 위임된 함수들
  putValue(value) {
    return this.calculator.putValue(value)
  }

  getValue() {
    return this.calculator.getValue()
  }

  peekValue() {
    return this.calculator.peekValue()
  }

  clear() {
    return this.calculator.clear()
  }

  multiply() {
    return this.calculator.multiply()
  }
}

const calculator = new StackCalculator()
const enhancedCalculator = new EnhancedCalculator(calculator)

enhancedCalculator.putValue(4)
enhancedCalculator.putValue(3)
console.log(enhancedCalculator.add())
enhancedCalculator.putValue(2)
console.log(enhancedCalculator.multiply())
  • wrappee의 메서드를 직접 수정하지 않고, 래핑 후에 호출하기만 함
  • 수동 위임해야함

객체 확장(augmentation)

function patchCalculator(calculator) {
  // 새로운 함수
  calculator.add = function () {
    const addend2 = calculator.getValue()
    const addend1 = calculator.getValue()
    const result = addend1 + addend2
    calculator.putValue(result)
    return result
  }

  // 수정된 함수
  const divideOrig = calculator.divide
  calculator.divide = () => {
    // 추가적인 검증 로직
    const divisor = calculator.peekValue()
    if (divisor === 0) {
      throw Error('Division by 0')
    }
    // Subject에 대한 유효한 위임자(delegate)일 경우
    return divideOrig.apply(calculator)
  }

  return calculator
}

const calculator = new StackCalculator()
const enhancedCalculator = new patchCalculator(calculator)
// ...
  • 몽키패치 방식이다. 즉, wrappee의 메서드를 직접 수정한다.
    • 몽키패치 방식은 wrappee를 사용하고 있던 코드에서 문제를 야기할 수 있으므로 기피하는 게 좋겠다.
  • 수동 위임을 하지 않아도 됨.

Proxy 객체

const enhancedCalculatorHandler = {
  get(target, property) {
    if (property === 'add') {
      // 새로운 함수
      return function add() {
        const addend2 = target.getValue()
        const addend1 = target.getValue()

        const result = addend1 + addend2
        this.putValue(result)
        return result
      }
    } else if (property ==='divide') {
      // 수정된 함수
      return function () {
        // 추가적인 검증 로직
        const divisor = target.peekValue()
        if (divisor === 0) {
          throw Error('Division by 0')
        }
        // Subject에 대한 유효한 위임지(delegates)일 경우
        return target.divide()
      }
    }

    // 위임된 함수들과 속성들
    return target[property]
  }
}

const calculator = new StackCalculator()
const enhancedCalculator = new Proxy(
  calculator,
  enhancedCalculatorHandler)
// ...

참고: 내장 Proxy 객체

  • ES2015 스펙에서 도입한 프록시 패턴 구현 방법.
    • 프록시 패턴이란?
      • 다른 객체에 대한 엑세스를 제어하는 객체
      • validation, 권한 검증, 캐싱, 느린 초기화, logging, remote object 등에 쓰이는 패턴
      • 데코레이터와 구현 방법은 비슷하며, JavaScript에선 거의 동일하다고 봐도 무방
      • JavaScript에선 경계가 모호하기 때문에, 두 패턴 명명법에 얽매이지 않고 사용하는 것을 권장
      • 데코레이터 패턴과 비교
        • 공통점: 한 객체가 일부 작업을 다른 객체에 위임하는 점
        • 차이점: 목적 - 프록시는 객체 접근 제어 목적, 데코레이터는 새로운 동작을 추가 목적
  • 고급 수준의 접근 제어를 제공
    • 예를 들어, 객체 키가 존재하는지 확인하는 동작 수행 등...

참고자료

Mario Cascaiaro, Luciano Mammino, Node.js 디자인패턴 바이블 3판

0개의 댓글