9장 계층형 설계 2
이번 장에서 살펴볼 내용
- 코드를 모듈화하기 위해 추상화 벽을 만드는 법을 배웁니다.
- 좋은 인터페이스가 어떤 것이고, 어떻게 찾는지 알아봅니다.
- 설계가 이만하면 되었다고 할 수 있는 시점을 압니다.
- 왜 계층형 설계가 유지보수와 테스트, 재사용에 도움이 되는지 이해합니다.
계층형 설계 패턴
- 패턴 1: 직접 구현 직접 구현은 계충형 설계 구조를 만드는 데 도움이 됩니다. 직접 구현된 함수를 읽을 때, 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절한 구체화 수준으로 해결해야 합니다. 만약 너무 구체적이라면 코드에서 나는 냄새입니다.
- 패턴 2: 추상화 벽 호출 그래프에 어떤 계층은 중요한 세부 구현을 감추고 인터페이스를 사용하여 코드를 만들면 높은 차원으로 생각할 수 있습니다. 고수준의 추상화 단계만 생각하면 되기 때문에, 두뇌 용량의 한계를 극복할 수 있습니다.
- 패턴 3: 작은 인터페이스 시스템이 커질수록 비즈니스 개념을 나타내는 중요한 인터페이스는 작고 강력한 동작으로 구성하는 것이 좋습니다. 다른 동작도 직간접적으로 최소한의 인터페이스를 유지하면서 정의해야 합니다.
- 패턴 4: 편리한 계층 계충형 설계 패턴과 실천 방법은 개발자의 요구를 만족시키면서 비즈니스 문제를 잘 풀 수 있어야 합니다. 소프트웨어를 더 빠르고 고품질로 제공하는 데 도움이 되는 계층에 시간을 투자해야 합니다. 그냥 좋아서 계층을 추가하면 안 됩니다. 코드와 그 코드가 속한 추상화 계층은 작업할 때 편리해야 합니다.
패턴 2: 추상화 벽
추상화 벽은 여러가지 문제를 해결하는데, 그중 하나는 팀 간 책임을 명확하게 나누는 것이다. 추상화 벽은 세부 구현을 감춘 함수로 이루어진 계층이며, 추상화 벽에 있는 함수를 사용할 때에는 구현을 전혀 몰라도 함수를 쓸 수 있다는 것.
출처: 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);
}
출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1
이미지를 보면 점선을 가로지르는 함수는 없다.
그렇다면 완전하지 않은 추상화 벽을 완전한 추상화 벽으로 만드려면 어떻게 하면 될까..? 바로 추상화 벽에 새로운 함수를 만들면 된다.
추상화 벽을 사용하면 좋은 상황
- 쉽게 구현을 바꾸기 위해: 구현에 대한 확신이 없는 경우 추상화 벽을 사용해 구현을 간접적으로 사용할 수 있어서 나중에 구현을 바꾸기가 쉬워진다.
- 코드를 읽고 쓰기 쉽게 만들기 위해: 세부적인 것을 신경 쓸 필요가 없어진다.
- 팀 간에 조율해야 할 것을 줄이기 위해
- 주어진 문제에 집중하기 위해: 해결하려는 문제의 구체적인 부분을 신경쓰지 않을 수 있다.
추상화 벽은 팀 간의 커뮤니케이션 비용을 줄이고, 복잡한 코드를 명확하게 하기 위해 전략적으로 사용해야 한다. 신경 쓰지 않아도 되는 것을 다루는 것이 추상화 벽의 핵심..!
패턴 3: 작은 인터페이스
인터페이스를 최소화하면 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있는데..
가령 시계 할인 마케팅을 구현한다고 생각해보자. 하나는 추상화 벽에 구현하는 방법, 다른 하나는 추상화 벽 위에 있는 계층에 구현하는 방법이 있다. 어떤 방법을 선택하는 것이 좋을까?
출처: 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;
}
추상화 벽을 작게 만들어야 하는 이유는?
- 추상화 벽에 코드가 많을수록 구현이 변경되었을 때 고쳐야 할 것이 많습니다.
- 추상화 벽에 있는 코드는 낮은 수준의 코드이기 때문에 더 많은 버그가 있을 수 있습니다.
- 낮은 수준의 코드는 이해하기 더 어렵습니다.
- 추상화 벽에 코드가 많을수록 팀 간 조율해야 할 것도 많아집니다.
- 추상화 벽에 인터페이스가 많으면 알아야 할 것이 많아 사용하기 어렵습니다.
상위 계층에 어떤 함수를 만들 때 가능한 현재 계층에 있는 함수로 구현하는 것이 작은 인터페이스를 실천하는 방법.
패턴 4: 편리한 계층
이 패턴은 보다 현실적이고 실용적인 측면을 다룬다.
추상화는 가능한 일과 불가능한 일의 차이를 나타내기도 한다. (마치 자바스크립트 언어가 기계어에 대한 추상화 벽을 제공하는 것처럼..) 하지만 비즈니스를 생각하는 개발자에게 완벽한 추상화는 비효율적일 수 있다. 따라서 지금 당장 편리하다면 설계는 잠시 놓아두고 그대로 두어보자..! 개발자로서 비즈니스 요구사항과 훌륭한 아키텍처를 쌓아나가는 건 어느 하나 놓칠 수 없기에.
출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1
호출 그래프: 기능적 요구사항과 비기능적 요구사항을 잘 나타냄 특히 비기능적 요구사항 3가지는
- 유지보수성 (maintainabilty): 요구 사항이 바뀌었을 때 가장 쉽게 고칠 수 있는 코드는 어떤 코드인가요?
- 규칙: 위로 연결된 것이 적은 함수가 바꾸기 쉽습니다.
- 핵심: 자주 바뀌는 코드는 가능한 위쪽에 있어야 합니다.
- 테스트성 (testablity): 어떤 것을 테스트하는 것이 가장 중요한가요?
- 규칙: 위쪽으로 많이 연결된 함수를 테스트하는 것이 더 가치 있습니다.
- 핵심: 아래쪽에 있는 함수를 테스트하는 것이 위쪽에 있는 함수를 테스트하는 것보다 가치 있습니다.
- 재사용성 (reusability): 어떤 함수가 재사용하기 좋나요?
규칙: 아래쪽에 함수가 적을수록 더 재사용하기 좋습니다.
핵심: 낮은 수준의 단계로 함수를 빼내면 재사용성이 더 높아집니다.
출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1
코드를 적절한 위치에 두면 유지보수 비용을 많이 줄일 수 있다.
- 그래프의 가장 위에 있는 코드, 즉 가장 높은 계층에 있는 코드가 가장 고치기 쉬움
- 가장 낮은 계층에 있는 함수는 상위 계층에 영향을 줌.
- 자주 바뀌는 코드는 그래프 위에 있을수록 좋다. 따라서 수정 사항이 잦은 상위 계층은 적게 유지하는 것이 유리.
패턴을 사용하면 테스트 가능성에 맞춰 코드를 계층화할 수 있다.
- 아래에 있는 코드는 테스트가 중요: 많은 코드가 가장 하위 계층 코드에 의존하기 때문.
- 아래에 있는 것은 자주 바뀌지 않기 때문에 테스트 코드도 자주 바뀌지 않음.
계층형 구조를 만들면 재사용성이 좋아진다.
- 낮은 계층으로 함수를 추출하면 재사용할 가능성이 많아짐.
- 아래쪽에 함수가 적을수록 더 재사용하기 좋음.
요점 정리
- 추상화 벽 패턴을 사용하면 세부적인 것을 완벽하게 감출 수 있어서 더 높은 차원에서 생각할 수 있게 된다.
- 작은 인터페이스 패턴을 사용하면 완성된 인터페이스에 가깝게 계층을 만들 수 있다.
- 편리한 계층 패턴을 이용하면 다른 패턴을 요구 사항에 맞게 사용할 수 있다.
- 호출 그래프 구조에서 규칙을 얻을 수 있다.