Skip to main content

9장 계층형 설계 2

written by
positiveko
positiveko 🏆Front End Engineer

이번 장에서 살펴볼 내용

  • 코드를 모듈화하기 위해 추상화 벽을 만드는 법을 배웁니다.
  • 좋은 인터페이스가 어떤 것이고, 어떻게 찾는지 알아봅니다.
  • 설계가 이만하면 되었다고 할 수 있는 시점을 압니다.
  • 왜 계층형 설계가 유지보수와 테스트, 재사용에 도움이 되는지 이해합니다.

계층형 설계 패턴

  • 패턴 1: 직접 구현 직접 구현은 계충형 설계 구조를 만드는 데 도움이 됩니다. 직접 구현된 함수를 읽을 때, 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절한 구체화 수준으로 해결해야 합니다. 만약 너무 구체적이라면 코드에서 나는 냄새입니다.
  • 패턴 2: 추상화 벽 호출 그래프에 어떤 계층은 중요한 세부 구현을 감추고 인터페이스를 사용하여 코드를 만들면 높은 차원으로 생각할 수 있습니다. 고수준의 추상화 단계만 생각하면 되기 때문에, 두뇌 용량의 한계를 극복할 수 있습니다.
  • 패턴 3: 작은 인터페이스 시스템이 커질수록 비즈니스 개념을 나타내는 중요한 인터페이스는 작고 강력한 동작으로 구성하는 것이 좋습니다. 다른 동작도 직간접적으로 최소한의 인터페이스를 유지하면서 정의해야 합니다.
  • 패턴 4: 편리한 계층 계충형 설계 패턴과 실천 방법은 개발자의 요구를 만족시키면서 비즈니스 문제를 잘 풀 수 있어야 합니다. 소프트웨어를 더 빠르고 고품질로 제공하는 데 도움이 되는 계층에 시간을 투자해야 합니다. 그냥 좋아서 계층을 추가하면 안 됩니다. 코드와 그 코드가 속한 추상화 계층은 작업할 때 편리해야 합니다.

패턴 2: 추상화 벽

추상화 벽은 여러가지 문제를 해결하는데, 그중 하나는 팀 간 책임을 명확하게 나누는 것이다. 추상화 벽은 세부 구현을 감춘 함수로 이루어진 계층이며, 추상화 벽에 있는 함수를 사용할 때에는 구현을 전혀 몰라도 함수를 쓸 수 있다는 것.

abstraction barrier 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1

추상화 벽을 통해 점선 위 아래로 함수가 나뉘는데, 마케팅팀과 개발팀은 추상화 벽 너머에 있는 함수를 어떻게 쓸지 신경 쓰지 않고 독립적으로 일할 수 있게 된다. 이건 마치 우리가 종종 사용하는 라이브러리나 API와 비슷하다고 말한다. 우리 라이브러리나 API를 사용할 때 그 내부가 어떻게 구현되었는지 신경쓸 필요 없이 가져다가 쓸 수 있는 것처럼, 추상화 벽은 책임을 명확하게 나눠준다.

배열이었던 장바구니 데이터 구조를 객체로 만드는 코드를 작성해보자.

function add_item(cart, item) {
// return add_element_last(cart, item);
return objectSet(cart, item.name, item);
}
function calc_total(cart) {
var total = 0; // for(var i = 0; i < cart.length; i++) { //     var item = cart[i]; //     total += item.price; // }
var names = Object.keys(cart);
for (var i = 0; i < names.length; i++) {
var item = cart[names[i]];
total += item.price;
}
return total;
}
function setPriceByName(cart, name, price) {
// var cartCopy = cart.slice();
// for(var i = 0; i < cartCopy.length; i++) {
//   if(cartCopy[i].name === name)
//     cartCopy[i] = setPrice(cartCopy[i], price);
// }
// return cartCopy;
if (isInCart(cart, name)) {
var item = cart[name];
var copy = setPrice(item, price);
return objectSet(cart, name, copy);
} else {
var item = make_item(name, price);
return objectSet(cart, name, item);
}
}
function remove_item_by_name(cart, name) {
// var idx = indexOfItem(cart, name);
// if(idx !== null)
//   return splice(cart, idx, 1);
// return cart;
return objectDelete(cart, name);
} // for(var i = 0; i < cart.length; i++) { //  if(cart[i].name === name) //    return i; // } // return null;

// function indexOfItem(cart, name) {
// }
function isInCart(cart, name) {
// return indexOfItem(cart, name) !== null;
return cart.hasOwnProperty(name);
}

추상화 벽이 있기 때문에 배열이었던 장바구니 데이터 구조를 객체로 완전히 바꿀 수 있었다. 이처럼 추상화 벽은 필요하지 않은 것은 무시할 수 있도록 돕는다.

리팩터링의 결과로 장바구니 함수들은 아래와 같이 한 줄짜리 코드가 되었다.

function add_item(cart, item) {
return objectSet(cart, item.name, item);
}
function gets_free_shipping(cart) {
return calc_total(cart) >= 20;
}
function cartTax(cart) {
return calc_tax(calc_total(cart));
}
function remove_item_by_name(cart, name) {
return objectDelete(cart, name);
}
function isInCart(cart, name) {
return cart.hasOwnProperty(name);
}

abstraction barrier 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1

이미지를 보면 점선을 가로지르는 함수는 없다.

그렇다면 완전하지 않은 추상화 벽을 완전한 추상화 벽으로 만드려면 어떻게 하면 될까..? 바로 추상화 벽에 새로운 함수를 만들면 된다.

추상화 벽을 사용하면 좋은 상황

  1. 쉽게 구현을 바꾸기 위해: 구현에 대한 확신이 없는 경우 추상화 벽을 사용해 구현을 간접적으로 사용할 수 있어서 나중에 구현을 바꾸기가 쉬워진다.
  2. 코드를 읽고 쓰기 쉽게 만들기 위해: 세부적인 것을 신경 쓸 필요가 없어진다.
  3. 팀 간에 조율해야 할 것을 줄이기 위해
  4. 주어진 문제에 집중하기 위해: 해결하려는 문제의 구체적인 부분을 신경쓰지 않을 수 있다.

추상화 벽은 팀 간의 커뮤니케이션 비용을 줄이고, 복잡한 코드를 명확하게 하기 위해 전략적으로 사용해야 한다. 신경 쓰지 않아도 되는 것을 다루는 것이 추상화 벽의 핵심..!

패턴 3: 작은 인터페이스

인터페이스를 최소화하면 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있는데..

가령 시계 할인 마케팅을 구현한다고 생각해보자. 하나는 추상화 벽에 구현하는 방법, 다른 하나는 추상화 벽 위에 있는 계층에 구현하는 방법이 있다. 어떤 방법을 선택하는 것이 좋을까?

abstraction barrier 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1

저자는 두 번째 방법으로 추상화 벽 위에 있는 계층에 만드는 것이 좋다고 말한다. 그 이유는.. 더 직접 구현에 가까운 방식이기 때문! 왜 첫 번째 방법을 사용하지 않는 것이 좋을까?

  • 첫 번째 방법으로 구현할 경우 시스템 하위 계층 코드가 늘어나기 때문에 좋지 않다.
  • 추상화 벽을 유지하긴 하지만 코드 자체가 개발팀을 위한 코드기 때문이다.
  • 추상화 벽에 새로운 함수가 늘어난다는 것은 계약이 하나 더 늘어난다는 뜻이기 때문이다.

따라서 새로운 기능을 만들 때 하위 계층에 기능을 추가하는 것보다 상위 계층에 만드는 것이 작은 인터페이스 패턴이라고 할 수 있다.

// 1.
function getsWatchDiscount(cart) {
var total = 0;
var names = Object.keys(cart);
for(var i = 0; i < names.length; i++) {
var item = cart[names[i]];
total += item.price;
}
return total > 100 &&
cart.hasOwnProperty("watch");
}
// 2.
function getsWatchDiscount(cart) {
    var total  = calcTotal(cart);
    var hasWatch = isInCart("watch");
    return total > 100 && hasWatch;
}

추상화 벽을 작게 만들어야 하는 이유는?

  1. 추상화 벽에 코드가 많을수록 구현이 변경되었을 때 고쳐야 할 것이 많습니다.
  2. 추상화 벽에 있는 코드는 낮은 수준의 코드이기 때문에 더 많은 버그가 있을 수 있습니다.
  3. 낮은 수준의 코드는 이해하기 더 어렵습니다.
  4. 추상화 벽에 코드가 많을수록 팀 간 조율해야 할 것도 많아집니다.
  5. 추상화 벽에 인터페이스가 많으면 알아야 할 것이 많아 사용하기 어렵습니다.

상위 계층에 어떤 함수를 만들 때 가능한 현재 계층에 있는 함수로 구현하는 것이 작은 인터페이스를 실천하는 방법.

패턴 4: 편리한 계층

이 패턴은 보다 현실적이고 실용적인 측면을 다룬다.

추상화는 가능한 일과 불가능한 일의 차이를 나타내기도 한다. (마치 자바스크립트 언어가 기계어에 대한 추상화 벽을 제공하는 것처럼..) 하지만 비즈니스를 생각하는 개발자에게 완벽한 추상화는 비효율적일 수 있다. 따라서 지금 당장 편리하다면 설계는 잠시 놓아두고 그대로 두어보자..! 개발자로서 비즈니스 요구사항과 훌륭한 아키텍처를 쌓아나가는 건 어느 하나 놓칠 수 없기에.

abstraction barrier 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1

호출 그래프: 기능적 요구사항과 비기능적 요구사항을 잘 나타냄 특히 비기능적 요구사항 3가지는

  1. 유지보수성 (maintainabilty): 요구 사항이 바뀌었을 때 가장 쉽게 고칠 수 있는 코드는 어떤 코드인가요?
  • 규칙: 위로 연결된 것이 적은 함수가 바꾸기 쉽습니다.
  • 핵심: 자주 바뀌는 코드는 가능한 위쪽에 있어야 합니다.
  1. 테스트성 (testablity): 어떤 것을 테스트하는 것이 가장 중요한가요?
  • 규칙: 위쪽으로 많이 연결된 함수를 테스트하는 것이 더 가치 있습니다.
  • 핵심: 아래쪽에 있는 함수를 테스트하는 것이 위쪽에 있는 함수를 테스트하는 것보다 가치 있습니다.
  1. 재사용성 (reusability): 어떤 함수가 재사용하기 좋나요?
  • 규칙: 아래쪽에 함수가 적을수록 더 재사용하기 좋습니다.

  • 핵심: 낮은 수준의 단계로 함수를 빼내면 재사용성이 더 높아집니다.

    abstraction barrier 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1

  • 코드를 적절한 위치에 두면 유지보수 비용을 많이 줄일 수 있다.

    • 그래프의 가장 위에 있는 코드, 즉 가장 높은 계층에 있는 코드가 가장 고치기 쉬움
    • 가장 낮은 계층에 있는 함수는 상위 계층에 영향을 줌.
    • 자주 바뀌는 코드는 그래프 위에 있을수록 좋다. 따라서 수정 사항이 잦은 상위 계층은 적게 유지하는 것이 유리.
  • 패턴을 사용하면 테스트 가능성에 맞춰 코드를 계층화할 수 있다.

    • 아래에 있는 코드는 테스트가 중요: 많은 코드가 가장 하위 계층 코드에 의존하기 때문.
    • 아래에 있는 것은 자주 바뀌지 않기 때문에 테스트 코드도 자주 바뀌지 않음.
  • 계층형 구조를 만들면 재사용성이 좋아진다.

    • 낮은 계층으로 함수를 추출하면 재사용할 가능성이 많아짐.
    • 아래쪽에 함수가 적을수록 더 재사용하기 좋음.

요점 정리

  • 추상화 벽 패턴을 사용하면 세부적인 것을 완벽하게 감출 수 있어서 더 높은 차원에서 생각할 수 있게 된다.
  • 작은 인터페이스 패턴을 사용하면 완성된 인터페이스에 가깝게 계층을 만들 수 있다.
  • 편리한 계층 패턴을 이용하면 다른 패턴을 요구 사항에 맞게 사용할 수 있다.
  • 호출 그래프 구조에서 규칙을 얻을 수 있다.