Ch9 에서는 나머지 설계패턴인 추상화 벽, 작은 인터페이스, 편리한 계층에 대해 알 수 있습니다.
Ch9
요약추상화 벽을 통해 중요한 세부 구현을 감추고 인터페이스를 제공한다.
추상화 벽 위의 함수는 세부 구현을 알 필요가 없다. 만약 새로운 기능을 추가해야 할 때 추상화 벽에 생성한다면 논의해야 할 사항도 많고 세부적인 구현까지 알아야 한다. 그러나 추상화 벽 위에 생성할 경우 굳이 많은 정보를 알 필요 없이 추상화 벽의 인터페이스만 골라 사용할 수 있다. 이렇게 인터페이스를 작게 구성하는 패턴을 작은 인터페이스라고 한다.
추상화를 어느 단계까지 구성해야 하는지는 상황에 따라 다르다. 만약 코드가 동작하는데 문제가 없다면 굳이 더 작게 쪼갤 필요가 없고 로직 파악이 어렵고 코드를 읽기 어렵다면 더 작게 계층을 분리할 필요가 있다는 것이다.
인터페이스를 최소화하면 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있다.
작은 인터페이스 패턴은 새로운 기능을 만들 때 하위 계층에 기능을 추가하거나 고치는 것보다 상위 계층에 만드는 것을 말한다.
추상화 벽 패턴을 사용시 세부적인 것을 완전히 감출 수 있어 더 높은 차원에서 생각할 수 있다.
호출 그래프 구조에서 규칙을 얻을 수 있다. 이 규칙으로 어떤 코드를 테스트하는 것이 가장 좋은지 유지보수나 재사용하기 좋은 코드는 어디에 있는 코드인지 알 수 있다.
기획자는 구체적인 구현 과정을 알 필요 없이 추상화 벽의 코드를 가져다 사용할 수 있다. 만약 추상화 벽이 잘 구성되어 있다면 데이터 구조를 변경시, 개발자는 기획자가 세부 구현을 알 필요 없으니 변경을 용이하게 할 수 있다.
예를 들어 기존 함수가 배열로 코드가 이루어져 있는데 해시 기반 코드로 바꿔 성능을 향상시키고자 객체 기반 코드로 변경시키려 할 때 추상화 벽의 코드만 수정하면 된다.
/// 배열 기반 장바구니 코드
function add_item(cart, item) {
return add_element_last(cart, item);
}
function calc_total(cart) {
var total = 0;
for(var i = 0; i < cart.length; i++) {
var item = cart[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;
}
function remove_item_by_name(cart, name) {
var idx = indexOfItem(cart, name);
if(idx !== null)
return splice(cart, idx, 1);
return cart;
}
function indexOfItem(cart, name) {
for(var i = 0; i < cart.length; i++) {
if(cart[i].name === name)
return i;
}
return null;
}
function isInCart(cart, name) {
return indexOfItem(cart, name) !== null;
}
/// 객체 기반 장바구니 코드
function add_item(cart, item) {
return objectSet(cart, item.name, item);
}
function calc_total(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;
}
function setPriceByName(cart, name, price) {
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) {
return objectDelete(cart, name);
}
function isInCart(cart, name) {
return cart.hasOwnProperty(name);
}
새 기능을 추가할때 추상화 벽에 만들거나 추상화 벽 위에 만들 수 있다.
// 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;
}
추상화 벽에 추가할 경우 완전한 추상화 벽이기에 추상화 벽을 잘 유지할 수 있다. 그러나 마케팅 팀이 구체적인 구현에 신경써야 하기에 직접 구현 측면에서 올바르지 않다.
추상화 벽에 만드는 함수는 개발팀과 마케팅 팀 사이 추가적으로 논의해야 할 사항이 많아진다. 이는 추상화 벽의 장점을 약화시킨다.
새 기능을 만들때 하위 계층에 기능을 추가하거나 고치는 것보다 상위 계층에 만드는 것을 작은 인터페이스 패턴이라고 한다. 작은 인터페이스 패턴을 사용하여 하위 계층을 고칠 필요 없이 상위 계층에서 문제를 해결할 수 있다.
아이템을 추가할때 로그를 남기면 될까?
function add_item(cart, item) {
logAddToCart(global_user_id, item);
return objectSet(cart, item.name, item);
}
여기에 문제가 있다. 로그가 액션이라는 사실때문인데 원래 계산이었던 아이템 추가 함수가 로그로 인해 액션이 되어 액션이 확산되는 결과가 만들어지게 되는 것이다.
그렇게 되면 기존 함수중 무료 배송 아이콘을 추가하는 update_shipping_icons
의 경우에는 장바구니에 제품을 추가하지 않는데도 하위 계층에 로그가 생성되니 로직상 올바르지 않게 동작하게 된다.
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);
if(gets_free_shipping(new_cart))
button.show_free_shipping_icon();
else
button.hide_free_shipping_icon();
}
}
그러니 이런 기능의 추가를 추상화 벽 위에 있는 계층에서 호출하도록 코드를 작은 인터페이스를 유지하게 작성해 볼 수 있다.
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);
logAddToCart();
}
기능적 요구사항 - 소프트웨어가 정확히 해야 하는 일
비기능적 요구사항 - 테스트를 어떻게 해야 할 것인지, 재사용을 잘 할 수 있느지, 유지보수하기 어렵지 않은지와 같은 요구사항
요구사항이 바뀌었을 때 가장 쉽게 고칠수 있는 코드는?
어떤 것을 테스트하는 것이 가장 중요한가?
하위 계층 코드를 테스트할수록 얻는 것이 더 오래간다.
어떤 함수가 재사용하기 좋은가?
아래에 있는 코드는 의존하는 코드도 많고 쉽게 변하지 않으니 재사용하기 용이하다. 아래쪽으로 가리키는 화살표가 많은 함수는 재사용하기 어렵다.
분류 | 유지 보수성 | 테스트 가능성 | 재사용성 |
---|---|---|---|
규칙 | 위로 연결된 것이 적은 함수가 바꾸기 쉽다. | 위쪽으로 많이 연결된 함수를 테스트하는 것이 더 가치있다. | 아래쪽에 함수가 적을수록 더 재사용하기 좋다. |
핵심 | 자주 바뀌는 코드는 가능한 위쪽에 있어야 한다. | 아래쪽에 있는 함수를 테스트하는 것이 위쪽에 있는 함수를 테스트하는 것보다 가치 있다. | 낮은 수준의 단계로 함수를 빼내면 재사용성이 더 높아진다. |