개발자 노트

functional하게 decorator pattern 구현 본문

컴퓨터 언어/함수형 프로그래밍

functional하게 decorator pattern 구현

jurogrammer 2021. 6. 19. 16:37

계기

이번은 좀 서론이 기네요... 넘어가셔도 좋습니다.

시작은 flux개념을 이해하는데에서 시작했습니다. 그래서 redux + canvas로 벽돌 깨기를 구현하고 있었습니다.

(뭐 어떻게 삽질하니 react를 제외하고 redux만 사용이 가능하더라구요? )

문제 - 상태 변경시, 관련된 renderer에게만 notify하고 싶다.

redux에서는 상태변경시 특정 상태 변경에 대해 subscribe중인 rendering 로직이 실행됩니다.

예를 들면 공이 벽돌과 충돌한 action이 있다면 벽돌이 부셔져야겠지요? 그래서 벽돌 상태를 inactive정도로 변경했다면 벽돌을 그리는 담당을 하는 render function만 실행해야겠죠?

그런데 redux에서는 rootState로 관리하고, rootState를 subscribe하면 subscribe할 때 넘겼던 모든 rendering 로직을 가진 callback function이 실행됩니다.(subscriber에게 모두 notify를 보낸다는거죠.)

결국 벽돌을 그리는 render function뿐만 아니라 공을 그리는 render function까지 실행된다는 겁니다. ㅜㅜ 얼마나 비효율적이에요. 그래서 고수분에게 자문을 구했죠

메모제이션

방법을 들어보니 이전 상태를 저장하고 상태가 변경됬다면 그때 선택적으로 렌더링하는 기법을 사용한다고 하더라구요. 데코레이터 패턴이나 프록시 패턴으로 구현하면 되겠다? 싶더라구요. 그래서 학습겸 데코레이터 패턴으로 구현해보고자 했습니다.

자바스크립트로 작성할건데 굳이 객체지향적으로 코딩해야하나 싶어서 함수형 프로그래밍으로 작성했습니다.

데코레이터 패턴, 객체지향에서 나온 디자인패턴 아닌가요?

우리가 알고 있는 잘 정립되어 있는 디자인 패턴은 객체지향언어에서의 디자인패턴입니다. 그래서 "함수형"과 "데코레이터 패턴"은 양립할 수 없는 개념 아니냐? 라고 말할 수 있겠는데...

제 생각은 다릅니다. 사실 데코레이터 패턴의 본질은 어떤 도메인의 개념이 기본 동작이 있고, 여기에 부가적인 동작을 추가해주는 형태이다 라는거죠. 이걸 객체지향적으로 어떻게 풀어나갈까? 이를 구현한게 GOF의 데코레이터 패턴이라고 봅니다.

따라서 기본 동작, 부가적인 동작을 추가해주는 형태를 functional하게 구현하면 그것도 데코레이터라고 말할 수 있다는 것이죠. 따라서 jurogrammer의 데코레이터 패턴을 구현해보겠습니다.

비교하여 보시라고 객체지향에서 데코레이터 패턴 구조를 첨부하겠습니다. (wiki 자료)

예제 - 커피 제조

예제를 통해서 보는게 좋겠지요? 흔한 예제로 도메인은 커피제조입니다.

아메리카노는 Concrete Component에 해당,

얼음, 시럽, 휘핑크림 등등은 Concrete Decorator에 해당합니다.

아메리카노에 얼음을 꾸며주면 아이스 아메리카노가 되는거지요.

전제

요구 사항

  • 아메리카노 제조시에 원두 종류좀 선택하게 해줘라!
  • 재료를 선택해서 메뉴를 만들 수 있게 해줘!

가정

  • 모든 커피 메뉴는 아메리카노에서 재료를 추가하여 만든다.
    • 얼음 + 우유 + 아메리카노 => 아이스 카페 라떼
    • 얼음 + 아메리카노 => 아이스 아메리카노
    • 얼음 + 바닐라시럽 + 우유 + 아메리카노 => 아이스 바닐라 라떼
  • 재료 종류
    • 설탕
    • 바닐라 시럽
    • 우유
    • 얼음
  • 메뉴
    • 아메리카노
    • 아이스 아메리카노
    • 아이스 바닐라 라떼
    • ..... 등등 코드에서 보여드리겠습니다.

코드 구현

아메리카노 구현 (Concrete Component)

function makeAmericano(beanType) {
    return `${beanType} 아메리카노`;
}

원두 종류를 받아서 커피를 만들어야 하니까 beanType paramter를 선언해주었습니다. 객체지향에서 데코레이터 패턴에서 보시듯, 기본 동작이므로 decorator 시켜줄 대상을 받지 않습니다. decoratee 그 자체죠.

재료 구현 (Concrete Decorator)

재료는 아메리카노에 추가해주어야겠지요? 우선 얼음을 보여드릴게요.

얼음

function addIce(americano) {
    return `얼음 추가한 ${americano}`;
}

ice를 넣는 행위는 decorator에 해당하구요, concrete component에 해당하는 americano에 ice를 넣어주어야 합니다. 따라서 parameter로 americano를 받습니다.

그리고 americano에 얼음을 추가하는 로직을 수행한 뒤에 꾸며준 아메리카노를 반환하죠.

바닐라시럽, 우유, 설탕도 동일합니다. 바로바로 보여드릴게요

바닐라 시럽

function addVanillaSyrup(americano) {
    return `바닐라 시럽 추가한 ${americano}`;
}

우유

function addMilk(americano) {
    return `우유 추가한 ${americano}`;
}

설탕

function addSugar(americano) {
    return `설탕 추가한 ${americano}`;
}

메뉴 제작 함수 구현(Client)

위키의 구조에는 안나와있지만, 결국 concrete decorator들과 concrete component를 조합해주어야 합니다. 이를 클라이언트 코드에서 작성해주는 것이죠.

커피 도메인에서는

조합하는 과정을 커피를 제조한다라 할 수 있으며,

조합의 결과를 커피의 메뉴라고 할 수 있겠습니다.

이 코드가 핵심일텐데요, 어떻게든 조합하는 코드를 구현할 수 있겠지만, 저는 reduce를 이용하여 factory method 형태로 구현하였습니다.

function makeMenu(americanoFun, ingredientFuncs = []) {
    return (beanType) => {
        let americano = americanoFun(beanType);
        return ingredientFuncs.reduce((addedAmericano, addIngredient) => addIngredient(addedAmericano), americano);
    }
}
  1. americano 제조와 재료들을 받아서 메뉴를 만드는 함수를 생성합니다.
  2. 그리고 실제 메뉴를 제조할 때 원두의 종류를 받아 커피를 만들어주죠.

설명을 드리자면 다음과 같습니다.

1. closure

makeMenu에서 반환하는 익명 함수에서 americanoFun은 makeMenu에서 받은 파라미터로, makeMenu에 의해 생성된 closure에 저장되어 있습니다. 따라서 미리 메뉴를 제조한 뒤에, 나중에 익명 함수를 사용하는 시점에서 원두 종류만 받아도 해당 메뉴를 만들 수 있습니다.

한편... 다시 함수를 반환하는 형태로 만들어서 argument 2번 application하도록 되어 curring아닌가? 라고 할 수 있겠습니다... curring의 정확한 의미는 multiple arguments -> single argument 로 바꾸는 기법이기 때문에 정확히는 curring을 하진 않았습니다.

2. reduce

실제 커피 제조는 reduce에서 이루어집니다. 초기 값을 americano로 두고, 연속하여 재료를 추가하는 함수를 americano에 apply하는 방식이죠. reduce의 의미에 맞게 상태변경을 계속 시켜주면 되겠다. 싶어서 위와같이 떠올렸어요.

상태 변경을 떠올린 배경은 다음과 같습니다.

reduce의 사전적 의미

  1. 차원의 축소
  2. 상태의 전이

2개가 서로 다른거 아닌가? 어떻게 같은 의미로 사용되지? 생각이 들 수 있습니다. 좀 더 깊게 들어가서 설명해볼게요.

결론부터 말하자면 원소들을(n+1차원) 대상으로 상태의 전이를 반복해주면 차원이 축소됩니다. ㅎㅎ; 제가 수학적으로 공부해서 말한게 아니라서 틀릴 수도 있겠는데요... 상태 변경에 대해 생각해볼게요.

  • 현재의 상태를 S1라고 봅시다.
  • 어떤 값 x가 입력되면 다른 상태인 S2로 변경된다.

라고 말할 수 있잖아요? 오토마타 이론 떠올려보세용(또는 경영과학을 배운 사람이라면 동적계획법)

이는 다음과 같이 표현될 수 있죠.

t(S_cur, x) =  S_nxt

t는 상태를 변경해주는 함수이구요, S_cur은 현재상태, x는 input 그리고 S_nxt는 다음 상태라고 말할 수 있습니다. 여기서 reduce의 2번 의미가 나타나고, redux에서 reducer는 이 2번의 의미를 나타냅니다.

1번 차원의 축소는요... collection.reduce의 구조는 reducer함수를 받는데, 다음과 같은 구조입니다.

reducer(acc,cur)

엥? 어디서 많이 보지 않았어요? t(S_cur, x) 이거랑 같죠. collection.reduce는 원소 e_i에서 input을 받고 acc를 반환합니다. 그리고 그 acc와 다시 reducer 함수를 e_(i+1)과 연산하는 형태이지요. reduce의 2번째 파라미터는 초기 값을 의미하며, default로 collection의 첫번째 원소로 설정되어 있습니다.

그래서 위와 같이 연산하다보면 list가 value가 되버립니다!!

위 예제에서는 상태의 전이 개념을 좀 더 살리긴 했네요.

메뉴 구현

아메리카노

let americano = makeMenu(makeAmericano);

설탕넣은 아메키라노

let sugarAmericano = makeMenu(makeAmericano, [addSugar]);

아이스 바닐라 라떼

let iceVanillaLate = makeMenu(makeAmericano, [addIce, addVanillaSyrup, addMilk])

2번째 argument로 재료를 넣어주는 함수만 딱딱 넣어주면 메뉴를 만들 수 있습니다.

손님을 받아볼까요?

손님: 아라비카 원두로 아메리카노 주세요

바리스타:

americano("아라비카"); // "아라비카 아메리카노"

손님: 로부스타 원두로 설탕 추가한 아메리카노 주세요

바리스타:

sugarAmericano("로부스타"); // "설탕 추가한 로부스타 아메리카노"

손님: 리베리카 원두로 아이스 바닐라 라떼 주세요

바리스타:

iceVanillaLate("리베리카") // "우유 추가한 바닐라 시럽 추가한 얼음 추가한 리베리카 아메리카노"

잘 출력되네요... ㅎㅎ;

전체코드

function makeAmericano(beanType) {
    return `${beanType} 아메리카노`;
}

function addSugar(americano) {
    return `설탕 추가한 ${americano}`;
}

function addVanillaSyrup(americano) {
    return `바닐라 시럽 추가한 ${americano}`;
}

function addMilk(americano) {
    return `우유 추가한 ${americano}`;
}

function addIce(americano) {
    return `얼음 추가한 ${americano}`;
}

function makeMenu(americanoFun, ingredientFuncs = []) {
    return (beanType) => {
        let americano = americanoFun(beanType);
        return ingredientFuncs.reduce((addedAmericano, addIngredient) => addIngredient(addedAmericano), americano);
    }
}

let americano = makeMenu(makeAmericano);
let sugarAmericano = makeMenu(makeAmericano, [addSugar]);
let iceVanillaLate = makeMenu(makeAmericano, [addIce, addVanillaSyrup, addMilk])

마지막으로,

정말 functional인가?

  • 함수 객체로 받고, 함수 반환하고
  • 외부 참조 없이, transparent하게 사이드 이펙트가 없다.
  • immutable하다.

but... ingredientsFun.reduce에서 객체지향 코드가 있어서 순수하게 functional이라고 보긴 어렵네요.

뭐... 나름 함수로 코드를 썼으니까 functional이라고.. 하긴 했는데 ㅎㅎ; 판단은 여러분에게 맡기겠습니다.

arrow function으로만 구현

(americanoFun, ingredientsFun) => (beanType) =>  ingredientsFun.reduce((addedAmericano, addIngredient) => addIngredient(addedAmericano), americanoFun(beanType))

가독성이 안좋네요 ㅎ;

합성 함수

ingredients 함수들만 미리 작업하고 나중에 americano를 argument로 던져주면 되는거 아닌가? 생각했거든요? 초기값 굳이 americano넘겨줄 필요없이요. 다음처럼 말이죠.

let ingredientsFunc = addIce ∘ addVanillaSyrup ∘ addMilk
ingredientsFunc(americano)

이거... 생각보다 훨씬 까다로운 문제입니다. 위처럼 구현하려고하면 타입문제가 발생하거든요. javascript에서는 이상한 값 반환하고요. monad개념이랑 연관되어 있는거 같던데... 나중에 이를 주제로 글을 써보겠습니다.

반응형

'컴퓨터 언어 > 함수형 프로그래밍' 카테고리의 다른 글

lazy evaluation  (0) 2021.08.08
closure  (0) 2021.08.08
lambda calculus - boolean 연산  (0) 2021.03.21
Lambda Calculus - Formal 정리 (Reduction)  (0) 2021.02.07
Lambda Calculus - Formal 정리  (0) 2021.01.24
Comments