조컴퓨터

챕터 5. 더 좋은 액션 만들기 본문

책읽기/쏙쏙 들어오는 함수형 코딩

챕터 5. 더 좋은 액션 만들기

챠오위 2022. 8. 8. 14:33

챕터 5에서 중점적으로 볼 내용은 ? 

- 암묵적 입력과 출력을 제거해서 재사용하기 좋은 코드를 만드는 방법 알아보기

- 복잡하게 엉킨 코드를 풀어 더 좋은 구조로 만드는 법 배우기

 

 

비즈니스 요구 사항과 설계를 맞추기

요구 사항에 맞춰 더 나은 추상화 단계 선택하기

액션에서 계산으로 리팩터링하는 과정은 단순하고 기계적이었다. 기계적인 리팩터링이 항상 최선의 구조를 만들어 주는 것은 아니다.

 

gets_free_shipping() 함수는 비즈니스 요구 사항으로 봤을 때 맞지 않는 부분이 있다. 요구 사항은 장바구니에 담긴 제품을 주문할 때 무료 배송인지 확인하는 것이다. 하지만 함수를 보면 장바구니로 무료 배송을 확인하지 않고 제품의 합계와 가격으로 확인하고 있다. 이것은 비즈니스 요구 사항과 맞지 않는 인자라고 할 수 있다.

 

function gets_free_shipping(total, item_price) {   // 두 가지 인자는 요구 사항과 맞지 않다.

   return item_price + total >= 20;

}

 

또한 중복된 코드도 있다. 합계에 제품 가격을 더하는 코드가 두 군데 있다. 중복이 항상 나쁜 것은 아니지만 코드에서 나는 냄새이다. 코드의 냄새(code smell) 는 나중에 문제가 될 수도 있다.

 

function calc_total(cart) {

   var total = 0;

   for (var i = 0; i < cart.length; i++) {

      var item = cart[i];

      total += item.price;   // gets_free_shipping() 함수의 장바구니 합계를 계산하는 코드(item + total) 가 중복이다.

   }

   return total;

}

 

다음 함수를 

gets_free_shipping(total, item_price

 

아래와 같이 바꾼다.

gets_free_shipping(cart)   // 이 인자는 장바구니가 무료 배송인지 알려준다.

 

그리고 calc_total() 함수를 재사용하여 중복을 없애 보자.

 

 

비즈니스 요구 사항과 함수를 맞추기

함수의 동작을 바꿨기 때문에 엄밀히 말하면 리팩터링이라고 할 수 없다

gets_free_shipping() 함수는 장바구니 값을 인자로 받아 합계가 20 보다 크거나 같은지 알려준다.

 

원래 코드 새 시그니처를 적용한 코드
function gets_free_shipping(total, item_price) {
   return item_price + total >= 20;
}
function gets_free_shipping(cart) {
   return calc_total(cart) >= 20;
}

 

바꾼 함수는 합계와 제품 가격 대신 장바구니 데이터를 사용한다. 장바구니는 전자상거래에서 많이 사용하는 엔티티(entity) 타입이기 때문에 비즈니스 요구 사항과 잘 맞는다.

 

함수 시그니처가 바뀌었기 때문에 사용하는 부분도 고쳐야 한다.

 

원래 코드 새 시그니처를 적용한 코드
function update_shipping_icons() {
   var buttons = get_buy_buttons_dom();
   for (var i = 0; i < buttons.length; i++) {
      var button = buttons[i];
      var item = button.item;
      if (gets_free_shipping(
               shopping_cart_total, item.price)) {

         button.show_free_shipping_icon();
      } else {
         button.hide_free_shipping_icon();
      }
   }
}
function update_shipping_icons() {
   var buttons = get_buy_buttons_dom();
   for (var i = 0; i < buttons.length; i++) {
      var button = buttons[i];
      var item = button.item;
      var new_cart = add_item(shopping_cart,
                                                  item_name, item.price);
      if (gets_free_shipping(new_cart)) {
         button.show_free_shipping_icon();
      } else {
         button.hide_free_shipping_icon();
      }
   }
}

 

gets_free_shipping() 함수가 잘 동작하도록 고쳤다. 이제 이 함수는 장바구니가 무료 배송인지 아닌지 알려준다.

 

 


add_item() 함수를 부를 때마다 cart 배열을 복사한다. 비용이 너무 많이 들지는 않는가 ?

- 그렇기도 하고 아니기도 하다. 배열을 바꾸는 것보다 비용이 더 드는 것은 맞다. 하지만 최신 프로그래밍 언어의 런타임과 가비지 컬렉터(Garbage Collector) 는 불필요한 메모리를 효율적으로 잘 처리한다. 그래서 신경 쓰지 않고 복사본을 만들 수 있다. 자바스크립트는 불변형 문자열 구조를 제공한다. 구 문자열을 합치면 항상 새로운 문자열을 만든다. 이때 모든 문자를 복사하지만, 비용에 대해 고민하지 않는다.

그리고 복사본을 사용할 때 잃는 것보다 얻는 것이 더 많다. 이 방법은 많은 장점이 있다. 책에서 확인하라. 


 

원칙 : 암묵적 입력과 출력은 적을수록 좋다

인자가 아닌 모든 입력은 암묵적 입력이고 리턴값이 아닌 모든 출력은 암묵적 출력이다. 암묵적 입력과 출력이 없는 함수를 계산이라 부른다.

 

계산을 만들기 위해 암묵적 입력과 출력을 없애는 원칙은 액션에도 적용할 수 있다. 액션에서 모든 암묵적 입력과 출력을 없애지 않더라도 암묵적 입력과 출력을 줄이면 좋다. 

 

어떤 함수에 암묵적 입력과 출력이 있다면 다른 컴포넌트와 강하게 연결된 컴포넌트라 할 수 있다. 다른 곳에서 사용할 수 없기 때문에 모듈이 아니다. 이런 함수의 동작은 연결된 부분의 동작에 의존한다. 암묵적 입력과 출력을 명시적으로 바꿔 모듈화된 컴포넌트로 만들 수 있다. 

 

암묵적 입력이 있는 함수는 조심해서 사용해야 한다. 앞서 세금 계산할 때 shopping_cart_total 전역변수를 변경했다. 만약 다른 곳에서 이 값을 쓴다면 무슨 일이 생길까 ? 원하는 값을 얻으려면 세금을 계산하는 동안 다른 코드가 실행되지 않는지 확인해야 한다.

 

암묵적 출력이 있는 함수 역시 조심해서 사용해야 한다. 압묵적 출력으로 발생하는 일을 원할 때만 쓸 수 있다. 만약 DOM 을 바꾸는 암묵적 출력이 있는 함수를 쓸 때 DOM 을 바꾸는 것이 필요없다면 어떻게 해야 할까 ? 결과는 필요하지만 다른 곳에 영향을 주기 싫다면 어떻게 해야 할까 ? 

 

암묵적 입력과 출력이 있는 함수는 아무 때나 실행할 수 없기 때문에 테스트하기 어렵다. 모든 입력값을 설정하고 테스트를 돌린 후에 모든 출력값을 확인해야 한다. 입력과 출력이 많을수록 테스트는 더 어려워진다.

 

계산은 암묵적 입력과 출력이 없기 때문에 테스트하기 쉽다. 모든 암묵적 입력과 출력을 없애지 못해 액션을 계산으로 바꾸지 못해도 암묵적 입력과 출력을 줄이면 테스트하기 쉽고 재사용하기 좋다.

 

 

암묵적 입력과 출력 줄이기

update_shipping_icons() 함수에 위의 원칙을 적용해 암묵적 입력과 출력을 줄여보자. 먼저 암묵적 입력을 명시적 입력으로 바꿔보자.

 

원래 코드 명시적 인자로 바꾼 코드
function update_shipping_icons() {
   var buttons = get_buy_buttons_dom();
   for (var i = 0; i < buttons.length; i++) {
      var button = buttons[i];
      var item = button.item;
      var new_cart = add_item(shopping_cart
                                                  item_name, item.price);
      if (gets_free_shipping(new_cart)) {
         button.show_free_shipping_icon();
      } else {
         button.hide_free_shipping_icon();
      }
   }
}
function update_shipping_icons(cart) {
   var buttons = get_buy_buttons_dom();
   for (var i = 0; i < buttons.length; i++) {
      var button = buttons[i];
      var item = button.item;
      var new_cart = add_item(cart
                                                  item_name,
                                                  item.price);
      if (gets_free_shipping(new_cart)) {
         button.show_free_shipping_icon();
      } else {
         button.hide_free_shipping_icon();
      }
   }
}

 

함수 시그니처가 달라졌기 때문에 호출하는 곳도 바뀌어야 한다.

 

원래 코드 암묵적 입력을 없앤 코드
function calc_cart_total() {
   shopping_cart_total = calc_total(shopping_cart);
   set_cart_total_dom();
   update_shipping_icons();
   update_tax_dom();
}
function calc_cart_total() {
   shopping_cart_total = calc_total(shopping_cart);
   set_cart_total_dom();
   update_shipping_icons(shopping_cart);
   update_tax_dom();
}

 

 

코드 다시 살펴보기

함수형 원칙을 더 적용할 수 있는 부분이 있는지 살펴보는 것도 중요하지만 중복이나 불필요한 코드가 있는지도 살펴봐야 한다.

 

function add_item_to_cart(name, price) {   // 구매하기 버튼을 누르는 경우

   shopping_cart = add_item(shopping_cart, name, price);

   calc_cart_total(shopping_cart);

}

 

function calc_cart_total(cart) {   // add_item_to_cart() 함수에 있어도 될 것 같은데 ?

   var total = calc_total(cart);

   set_cart_total_dom(total);

   update_shipping_icons(cart);

   update_tax_dom(total);

   shopping_cart_total = total;   // 전역변수에 값을 할당했지만 읽는 곳이 없어 불필요한 코드
}

 

function set_cart_total_dom(total) {

   ...

}

 

function update_shipping_icons(cart) {
   var buttons = get_buy_buttons_dom();
   for (var i = 0; i < buttons.length; i++) {
      var button = buttons[i];
      var item = button.item;
      var new_cart = add_item(cart, item_name, item.price);
      if (gets_free_shipping(new_cart)) {
         button.show_free_shipping_icon();
      } else {
         button.hide_free_shipping_icon();
      }
   }
}

 

function update_tax_dom(total) {

   set_tax_dom(calc_tax(total));

}

 

정리할 코드가 두 개 있다. 사용하지 않는 shopping_cart_total 전역변수와 과해 보이는 calc_cart_total() 함수이다. 

 

원래 코드 개선한 코드
function add_item_to_cart(name, price) {
   shopping_cart = add_item(shopping_cart, 
                                                  name, price);
   calc_cart_total(shopping_cart);
}

function calc_cart_total(cart) {
   var total = calc_total(cart);
   set_cart_total_dom(total);
   update_shipping_icons(cart);
   update_tax_dom(total);
   shopping_cart_total = total;
}
function add_item_to_cart(name, price) {
   shopping_cart = add_item(shopping_cart, 
                                                  name, price);

   var total = calc_total(shopping_cart));
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart));
   update_tax_dom(total);
}

 

나머지 코드는 고칠 부분이 없어 표시하지 않았다. 액션이 많이 좋아졌기 때문에 이제 의미 있는 계층으로 나누는 방법에 대해 알아보자.

 

 

계산 분류하기

의미 있는 계층에 대해 알아보기 위해 계산을 분류해 보자.

cart 에 대한 동작은 C 라 표시하고, item 에 대한 동작은 I 라 표시하자. 그리고 비즈니스 규칙에 대한 함수는 B 라고 표시하자.

 

function add_item(cart, name, price) {   C  I

   var new_cart = cart.slice();

   new_cart.push({

      name : name,

      price : price

   });

   return new_cart;

}

 

function calc_total(cart) {   C  I  B

   var total = 0;

   for (var i = 0; i < cart.length; i++) {

      var item = cart[i];

      total += item.price;

   }

   return total;

}

 

function gets_free_shipping(cart) {   B

   return calc_total(cart) >= 20;

}

 

function calc_tax(amount) {   B

   return amount * 0.10;

}

 

시간이 지날수록 나눈 그룹은 더 명확해질 것이다. 그리고 이렇게 나눈 것은 코드에서 의미있는 계층이 되기 때문에 기억해두면 좋다. 

 

 

원칙 : 설계는 엉켜있는 코드를 푸는 것이다

함수를 사용하면 관심사를 자연스럽게 분리할 수 있다. 다시 합칠 수도 있다. 

 

재사용하기 쉽다.

함수는 작으면 작을수록 재사용하기 쉽다.

 

유지보수하기 쉽다.

작은 함수는 쉽게 이해할 수 있고 유지보수하기 쉽다.

 

테스트하기 쉽다.

작은 함수는 테스트하기 쉽다. 한 가지 일만 하기 때문에 한 가지만 테스트하면 된다.

 

함수에 특별한 문제가 없어도 꺼낼 것이 있다면 분리하는 것이 좋다. 그렇게 하면 더 좋은 설계가 된다.

 

 

add_item() 을 분리해 더 좋은 설계 만들기

function add_item(cart, name, price) {

   var new_cart = cart.slice();   // 1. 배열을 복사한다

   new_cart.push({   // 2. item 객체를 만든다 & 3. 복사본에 item 을 추가한다

      name : name,

      price : price

   });

   return new_cart;   // 4. 복사본을 리턴한다

}

 

 

item 에 관한 코드를 별도의 함수로 분리해 보자.

 

원래 코드 개선한 코드
function add_item(cart, name, price) {
   var new_cart = cart.slice();
   new_cart.push({
      name : name,
      price : price
   });
   return new_cart;
}

add_item(shopping_cart, "shoes", 3.45);
// 2. item 객체를 만든다.
function make_cart_item(name, price) {

   return {
      name : name,
      price : price
   };
}

function add_item(cart, item) {
   var new_cart = cart.slice();   // 1. 배열을 복사한다.
   new_cart.push(item);   // 3. 복사본에 item 을 추가한다.
   return new_cart;   // 4. 복사본을 리턴한다.
}

add_item(shopping_cart, make_cart_item("shoes", 3.45));

 

item 구조만 알고 있는 함수(make_cart_item) 와 cart 구조만 알고 있는 함수(add_item) 로 나누어 원래 코드를 고쳤다. 이렇게 분리하면 cart 와 item 을 독립적으로 확장할 수 있다. 예를 들어 배열인 cart 를 해시 맵 같은 자료 구조로 바꾼다고 할 때 변경해야 할 부분이 적어진다.

 

1번, 3번, 4번은 값을 바꿀 때 복사하는 카피-온-라이트(copy-on-write) 를 구현한 부분이기 때문에 함께 두는 것이 좋다.

 

add_item() 함수는 cart 와 item 에 특화된 함수가 아니다. 일반적인 배열과 항목을 넘겨도 잘 동작한다.

 

 

카피-온-라이트 패턴을 빼내기

이제 add_item() 함수는 크기가 작고 괜찮은 함수이다. 자세히 보면 카피-온-라이트 패턴을 활용해 배열에 항목을 추가하는 함수이다. 이 함수는 일반적인 배열과 항목에 쓸 수 있지만 이름은 일반적이지 않다. 이름만 보면 장바구니를 넘겨야 쓸 수 있을 것 같다.

 

function add_item(cart, item) {   // 일반적인 이름이 아니다
   var new_cart = cart.slice();
   new_cart.push(item);
   return new_cart;
}

 

함수 이름과 인자 이름을 더 일반적인 이름으로 바꿔 보자.

 

원래 코드(일반적이지 않은 이름) 개선한 코드
function add_item(cartitem) {
   var new_cart = cart.slice();
   new_cart.push(item);
   return new_cart;
}
function add_element_last(arrayelem) {
   var new_array = array.slice();
   new_array.push(elem);
   return new_array;
}

 

원래 add_item() 함수는 간단하게 다시 만들 수 있다.

 

function add_item(cart, item) {
   return add_element_last(cart, item);
}

 

장바구니와 제품에만 쓸 수 있는 함수가 아닌 어떤 배열이나 항목에도 쓸 수 있는 이름으로 바꿨다. 이 함수는 재사용할 수 있는 유틸리티(utility) 함수이다. 앞으로 장바구니에 제품을 추가하는 것뿐만 아니라 배열에 항목을 추가할 일이 있을 것이다. 그때도 변경 불가능한 배열이 필요할 것이다. 

 

 

add_item() 사용하기

add_item() 는 cart, name, price 인자가 필요한 함수였다.

 

function add_item(cart, name, price) {
   var new_cart = cart.slice();
   new_cart.push({
      name : name,
      price : price
   });
   return new_cart;
}

 

이제 cart 와 item 인자만 필요하다.

 

function add_item(cart, item) {
   return add_element_last(cart, item);
}

 

그리고 item 을 만드는 생성자 함수를 분리했다.

 

function make_cart_item(name, price) {
   return {
      name : name,
      price : price
   };
}

 

그래서 add_item() 을 호출하는 곳에서 올바른 인자를 넘기도록 고쳐야 한다.

 

원래 코드 고친 함수를 사용하는 코드
function add_item_to_cart(name, price) {
   shopping_cart = add_item(shopping_cart, 
                                                  name, price);

   var total = calc_total(shopping_cart));
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart));
   update_tax_dom(total);
}
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, 
                                                  item);

   var total = calc_total(shopping_cart));
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart));
   update_tax_dom(total);
}

 

item 을 만들어 add_item() 함수에 넘겨줬다. 이제 코드를 고치는 작업은 끝났다. 

 

 

계산을 분류하기

cart 에 대한 동작은 C 라 표시하고, item 에 대한 동작은 I 라 표시하자. 그리고 비즈니스 규칙에 대한 함수는 B 라고 표시하자. 마지막으로 배열 유틸리티 함수는 A 라고 표시하자.

 

function add_element_last(array, elem) {   A

   var new_array = array.slice();

   new_array.push(elem);

   return new_array;

}

 

function add_item(cart, item) {   C

   return add_element_last(cart, item);

}

 

function make_cart_item(name, price) {   I

   return {

      name : name,

      price : price

   };

}

 

function calc_total(cart) {   C  I  B

   var total = 0;

   for (var i = 0; i < cart.length; i++) {

      var item = cart[i];

      total += item.price;

   }

   return total;

}

 

function gets_free_shipping(cart) {   B

   return calc_total(cart) >= 20;

}

 

function calc_tax(amount) {   B

   return amount * 0.10;

}

 


왜 계산을 유틸리티와 장바구니, 비즈니스 규칙으로 다시 나누는 것인가 ? 

- 이렇게 나누는 이유는 나중에 다룰 설계 기술을 미리 보여주기 위해서이다. 최종적으로는 코드를 구분된 그룹과 분리된 계층으로 구성할 것이다.

 

그러면 비즈니스 규칙과 장바구니 기능은 어떤 차이가 있는가 ? 전자상거래를 만드는 것이라면 장바구니에 관한 것은 모두 비즈니스 규칙이 아닌 건가 ?

- 장바구니는 대부분 전자상거래 서비스에서 사용하는 일반적인 개념이다. 그리고 장바구니가 동작하는 방식도 모두 비슷하다. 하지만 비즈니스 규칙은 다른다. MegaMart 에서 운영하는 특별한 규칙이라고 할 수 있다. 예를 들어 다른 전자상거래 서비스에도 장바구니 기능이 있을 것이라고 기대하지만, MegaMart 와 똑같은 무료 배송 규칙이 있을 것이라고는 기대하지 않는다.

 

비즈니스 규칙과 장바구니에 대한 동작에 모두 속하는 함수도 있을 수 있는가 ?

- 예라고 할 수는 있다. 하지만 계층에 관점에서 보면 코드에서 나는 냄새이다. 비즈니스 규칙에서 장바구니가 배열인지 알아야 한다면 문제가 될 수 있다. 비즈니스 규칙은 장바구니 구조와 같은 하위 계층보다 빠르게 바뀐다. 설계를 진행하면서 이 부분은 분리해야 한다. 하지만 지금은 그대로 두겠다. 


 

작은 함수와 많은 계산(추가 예정)

 

 

출처 : 쏙쏙 들어오는 함수형 코딩(Grokking Simplicity: Taming complex software with functional thinking)