구조적 설계 디자인 패턴

Chang-__-·2023년 3월 20일
0

디자인패턴

목록 보기
2/3
post-thumbnail

프록시

프록시는 Subject 라고 하는 다른 객체에 대한 엑세스를 제어하는 객체입니다. 프록시는 Subject에 대해 실행되는 작업의 전부 또는 일부를 가로채서 해당 동작을 증강하거나 보완하도록 합니다.

프록시는 각 작업을 Subject에 전달하여 전처리나 후처리를 통해 기능을 매끄럽게 만들 수 있다.
프록시를 사용하면 좋은 상황이 몇가지 있다.

1. 데이터 검증: 프록시가 데이터를 Subject로 전달하기 전에 유효성을 검사한다.
2. 보안: 프록시는 클라이언트가 작업을 수행할 권한이 있는지 확인하고, 권한이 있는경우에만 요청을 Subject에 전달한다.
3. 캐싱: 데이터가 아직 캐시에 없는 경우에만 프록시 작업이 Subject에서 실행되도록 프록시는 내부에 캐시를 유지한다.
4. 기록: 프록시는 메서드 호출과 관련 매개 변수를 가로채서 발생시 데이터를 기록한다.
5. 느린 초기화: Subject를 생성할때 cost가 클 경우, 
			 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 객체가 생성된 것처럼 동작하도록 하고싶을 때 사용한다.

객체의 확장

class Subject {
  do(){}
}

class Real extends Subject {
  do() {
    return 'do';
  }
}

class Proxy extends Subject {
  real;
  do() {
    if(!this.real) {
      this.real = new Real();
    }
    return this.real.do();
  }
}

proxy = new Proxy();
console.log(proxy.do());

Proxy의 객체를 통해 접근을 하며, Proxy 에서 Real 객체를 접근한다. 여기서 Proxy를 통해 접근을 하면 후처리나 전처리가 가능하게 되는데 객체의 확장이 가능해진다.

Javascript 에서 프록시 객체

ES2015에서 프록시 객체를 만드는 Proxy 라는 객체가 등장했다. 사용방법은 다음과 같다.

const proxy = new Proxy(target, handler)

여기서 target은 프록시가 적용될 객체(Subject)를 나타내며, handler는 프록시의 동작을 정의하는 객체다.
hanlder 객체에는 (apply, get, set, has)등 여러가지 트랩을 제공하는데, 코드를 작성하면서 설명을 하도록 하겠다.

데이터를 전부 encrypt 해야하는 proxy는 이렇게 구현할 수 있다.

import crypto from "crypto";

const obj = {
    name: 'ian',
    email: 'ian@test.com',
}

const encrypt = (obj) => {
    const encrypted = {}
    for (const i of Object.keys(obj)) {
        encrypted[i] = crypto.createHash('sha512')
            .update(obj[i])
            .digest('base64');
    } //1
    return new Proxy(encrypted, { 
        set(target, key, value) { //2
            return target[key] =  crypto.createHash('sha512')
                .update(value)
                .digest('base64');
        },
        get(target, key){ //3
            return target[key]
        }
    })
}

const person = encrypt(obj);
person.password = "IanPassword"; //4
console.log(person)

이 결과를 출력하면 다음과 같다.

{
  name: 'OlphqEDmcxZWJsWoHYBwvMfBMdJjeiw8LCJEULsXoScBBDcv03sSrr66x4gBwkWGs+k2odYg8SZ2SEQ1yXorAQ==',
  email: '/fDulJCncShdB5DJ4/s00Ioxk/JrndaJ2YSitD41mEcKIC4ykE0qGMoMlrGEXId4K0cebLzCbhqAe+uXJsPzYw==',
  password: 'jTpvjpBqjmkHPZNrs/vV/c42yBVvKKhmkjULf+AlWEJcwkZjUOCho4LDD44VEJJ/S6NWiPD2rjhgU9zbIzzmCg=='
}

왜 이렇게 되는지에 대해서 간단히 설명을 해보면 다음과 같다.

1. object에 있는 모든 요소를 encrypt 한다.
2. Proxy 객체를 활용해서 새로 셋팅되는 key 값들은 set 메소드를 사용해서 암호화 되도록 한다.
3. 객체 Property 에 접근할 때 마다 proxy를 탄다.
4. object에 새로운 프로퍼티가 추가 될때 마다 값을 encoding 한다.

Proxy 객체를 통한 옵저버

Proxy 를 통해서 observable 한 객체도 만들수 있다. 여기서 observable 이란, Subject가 하나 이상의 옵저버에게 상태 변경을 알리는 디자인 패턴으로 변경 사항이 발생하는 즉시 반응을 하는것이다.

코드로 예를들어보면 다음과 같다.

function createObservable (target, observer) {
  const observable = new Proxy(target, {
    set (obj, prop, value) {
      if (value !== obj[prop]) {
        const prev = obj[prop] // 바뀌기전의 상태를 저장
        obj[prop] = value // 들어온 값을 obj에 저장
        observer({ prop, prev, curr: value }) //객체의 key, 바뀌기 전값, 바뀐값을 return 
      }
      return true
    }
  })

  return observable
}

이런 observer를 하나 만든 다음에 다음과 같이 사용해보겠다.

import { createObservable } from './create-observable.js'

function calculateTotal (invoice) {
  return invoice.subtotal -
    invoice.discount +
    invoice.tax
}

const invoice = {
  subtotal: 100,
  discount: 10,
  tax: 20
}

let total = calculateTotal(invoice)
console.log(`Starting total: ${total}`)

const obsInvoice = createObservable(
  invoice,
  ({ prop, prev, curr }) => {
    total = calculateTotal(invoice)
   console.log(`TOTAL: ${total} (${prop} changed: ${prev} -> ${curr})`)
  }
)

obsInvoice.subtotal = 200 // TOTAL: 210
obsInvoice.discount = 20 // TOTAL: 200
obsInvoice.discount = 20 // no change: doesn't notify
obsInvoice.tax = 30 // TOTAL: 210
console.log(`Final total: ${total}`)

실행을 해보면 다음과 같이 나온다.

Starting total: 110
TOTAL: 210 (subtotal changed: 100 -> 200)
TOTAL: 200 (discount changed: 10 -> 20)
TOTAL: 210 (tax changed: 20 -> 30)
Final total: 210

실제로 각각의 property들이 reactive 하게 값이 변경된것을 확인 할수 있다.

데코레이터

데코레이터는 기존 객체의 동작을 동적으로 증대시키는 것으로 구성된 구조적인 디자인 패턴이다.

동작이 해당 클래스의 모든 객체에 적용되지 않고 명시적으로 데코레이팅된 인스턴스에만 추가되기 때문에 클래의 상속과는 다르다.

디자인원칙 OCP : 클래스는 확장자에 대해서는 열려있어야 하지만 코드 변경에 대해서는 닫혀있어야 한다.

즉 기존 코드는 건드리지 말고 확장을 통해서 새로운 행동을 간단하게 추가할 수 있어야 한다.

서브클래스를 통해 확장

데코레이터 패턴을 설명할 떄 가장 많은 예로 등장하는 카페 주문에 대해서 살펴 보겠다.
아메리카노를 주문하고 여러가지 재료를 첨가하는 상황이다. (우유추가 샷추가 크림추가)

class Beverage {
  cost() {
    let total = 0;

    if (hasMilk()) total += 300;
    if (hasShot()) total += 500;
    if (hasCream()) total += 300;
    if (hasCookie()) total += 1000;
  };
}
// 그리고 커피 클래스 에서는
class CaffeLatte extends Beverage {
  constructor() {
    super();
  }

  cost() {
    return super.cost() + 5000;
  };
}

여기서 문제는 재료를 추가할 때 하마다 슈퍼클래스를 계속 수정해야한다.
위에 설명한것 처럼 기존 코드는 건드리지 말고 확장을 통해서 새로운 행동을 간단하게 추가할 수 있어야 한다.
그럼 어떻게 확장시켜야 할까?

class Beverage {
   cost() {};
}

// 위 클래스는  건드리지 않습니다.

class DecoratorBeverage extends Beverage {
  cost() {
    throw 'You must be override this function!';
  }
}

class Americano extends DecoratorBeverage {
  cost() {
    return 4000;
  }
}

class CaffeLatte extends DecoratorBeverage {
  cost() {
    return 5000;
  }
}

이렇게 종류를 다양하게 해놓고 이번엔 재료 클래스를 만들어 보겠다.

class Cream extends DecoratorBeverage {
    constructor(beverage) {
        super();
        this.beverage = beverage;
    }

    cost() {
        return this.beverage.cost() + 300;
    }
}

class Shot extends DecoratorBeverage {
    constructor(beverage) {
        super();
        this.beverage = beverage;
    }

    cost() {
        return this.beverage.cost() + 500;
    }
}

class Milk extends DecoratorBeverage {
    constructor(beverage) {
        super();
        this.beverage = beverage;
    }

    cost() {
        return this.beverage.cost() + 300;
    }
}

그 다음 실제로 사용을 해보겠다.

let americano = new Americano();
americano = new Shot(americano);
americano = new Milk(americano);

console.log('cost : ', americano.cost()); // 4800

javascript 에서 decorator

javascript 에서 babel과 같이 트랜스파일러의 도움을 통해 decorator를 사용할 수 있다. (typescript를 사용한다면 호환이 더 잘되긴한다.)
반면 타입스크립트 진영에서는 이미 적극적으로 데코레이터를 도입하여 사용하고 있는데 예로들면 Nest.js나 TypeORM 같은 라이브러리가 있다.

아래와 같이 함수 호출시 다른 함수를 래핑하여 Decorator를 구현할 수 있습니다.

function doSomething(message) {
    console.log(`${message}`);
}

function loggingDecorator(wrapper) {
    return function() {
        console.log('logging start');
        const result = wrapper.apply(this, arguments);
        console.log('logging finished');
        return result;
    }
}

const wrapper = loggingDecorator(doSomething);

wrapper('Hello!');
// 'logging start'
// 'Hello!'
// 'logging finished'

참고로 applyarguments는 javascript에서 기본적으로 제공하는 함수와 프로퍼티이다.

그렇다면 실제로 decorator를 어떻게 사용할까

function readonly(target, key, descriptor) {
    return {
        ...descriptor,
        writable: false,
    };
}

class Data {
    @readonly
    password = 'pwd';
}

const data = new Data();
data.password = 'newpwd';

@ 키워드를 통해서 decorator를 사용할수 있고, 함수를 따로 만들어서 여기서 descriptor 가 중요한데 MDN 문서를 참고해보면 좋겠다.

참고로 typescript를 사용하면 더욱 다양한 decorator를 만들수 있다. (링크)

프록시와 데코레이터의 경계

고전적인 정의로는 데코레이션은 새로운 동작을 기존의 객체에 추가할 수 있는 케머니즘으로 정의하고 있으며, 반면 프록시 패턴은 고정적이거나 가상의 객체에 접근을 제어하는데 사용된다.
데코레이터 패턴은 사실 래퍼로 볼수 있으며 다양한 유형의 객체를 가져와 데코레이터로 감싸서 추가인 기능을 추가할 수 있다.
프록시는 대신 객체에 대한접근을 제어하는데 사용되며 원래의 인터페이스를 변경하진 않는다.

어뎁터

어댑터 패턴을 사용하면 다른 인터페이스로도 객체의 기능을 사용할 수 있다.
예를 들어 110V 를 사용하는데 내가 가진 충전기는 220V 라면 110V로 바꿔주는 어뎁터를 사용해야 할것이다. 일반적인 의미에서 어댑터는 다른 인터페이스를 사용하는 컨텍스트에서 사용할 수 있도록 객체의 인터페이스를 변환시켜 준다.

아래와 같이 글자를 출력하는 프린터 클래스가 있다고 가정 해보자.

class Printer {
    constructor() {
        this.textArr = [];
    }

    pushText(text) {
        this.textArr.push(text);
    }

    print() {
        return this.textArr.join(' ');
    }
}

const printer = new Printer();
printer.pushText('Hello');
printer.pushText('World!');
console.log(printer.print()); // Hello World!

여기까지는 문제가 없을텐데 만약에 #를 붙혀서 출력하는 프린터가 있다고 가정을 해보자.

class HashTagPrinter {
    constructor() {
        this.textArr = [];
    }

    pushText(text) {
        this.textArr.push(text);
    }

    printWithHashTag() {  // print -> printWithHashTag로 변경
        return this.textArr.map(text => `#${text}`).join(' ');
    }
}

const printer = new HashTagPrinter();
printer.pushText('Hello');
printer.pushText('World!');
console.log(printer.print()); //오류 발생 HashTagPrinter에는 print 함수가 없음

데코레이터 부분에서도 설명했는데, 코드의 수정에 대해선 닫혀있어야 한다.
여기서 HashTagPrinter가 print를 할 수 있도록 해야하는데 그러면 어떻게 변경시켜줄 수 있을까?
물론 printer.printWithHashTag 함수를 사용해도 된다. 하지만 문제는 실제 코드에서는 저 부분만 수정한다는 보장이 없을뿐더러, 만약 우리가 다시 이전프린터를 사용한다고 하면 같은곳을 또 수정 해야한다.

그래서 우리는 어뎁터를 쓰는데 어뎁터를 명시해주겠다.

class HashTagAdapter {
    constructor(hashTagPrinter) {
        this.printer = hashTagPrinter;
    }

    pushText(text) {
        this.printer.pushText(text);
    }

    print() {
        return this.printer.printWithHash(); // 220V -> 110V 변환!
    }
}

그러면 사용은 어떻게 해야할까?

class HashTagPrinterAdapter {
    constructor(hashTagPrinter) {
        this.printer = hashTagPrinter;
    }

    pushText(text) {
        this.printer.pushText(text);
    }

    print() {
        return this.printer.printWithHash();
    }
}

let printer = new HashTagAdapter(new HashTagPrinter());
printer.pushText('Hello');
printer.pushText('World!');

console.log(printer.print()); // #Hello #World!

0개의 댓글