계층형 설계 II
코드가 점점 복잡해지는 가장 흔한 이유는 세부 구현과 상위 로직이 뒤섞여 있기 때문입니다.
이를 풀어내는 방법 중 하나가 바로 계층형 설계(Stratified Design) 입니다.
이번 글에서는 계층형 설계에서 추상화 벽과 작은 인터페이스, 그리고 설계를 어느 지점에서 멈춰야 하는지 알려주는 편리한 계층에 대해 살펴보겠습니다.
스터디 회차: 8회차 (2025년 8월 28일)
추상화 벽 (Abstraction Barrier)
- 추상화 벽은 세부 구현을 감춘 채, 상위 계층에서 간단히 활용할 수 있도록 제공되는 함수들의 집합입니다.
- 이 벽을 기준으로 위와 아래는 서로의 내부 동작을 몰라도 되며, 독립적으로 움직일 수 있습니다.
예제: 장바구니 데이터 구조 바꾸기
tsx
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 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;
}
- 배열은 아이템을 찾을 때 순차적으로 검색해야 하므로 성능이 떨어집니다.
- 따라서 배열 대신 객체를 해시 맵처럼 사용하는 것이 더 효율적입니다.
개선하기
tsx
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 remove_item_by_name(cart, name) {
return objectDelete(cart, name);
}
- 장바구니를 객체 구조로 바꾸어 코드를 개선합니다.
indexOfItem
함수는 더 이상 필요하지 않으므로 제거했고,remove_item_by_name
함수도 훨씬 단순해졌습니다.
결과
- 데이터 구조는 바뀌었지만, 추상화 벽 위에 있는 함수는 전혀 손대지 않았습니다.
- 추상화 벽은 필요 없는 세부사항을 감추고, 상위 계층이 내부 구현과 무관하게 동작할 수 있도록 만듭니다.
- 덕분에 상위 로직은 그대로 둔 채 내부만 교체할 수 있었고, 이는 곧 변화에 강한 구조를 만든다는 의미입니다.
remove_item_by_name
함수는 한줄짜리인데 필요할까요?
ts
function remove_item_by_name(cart, name) {
return objectDelete(cart, name);
}
코드 줄수는 중요하지 않습니다. 적절한 구체화 수준과 일반화가 되어있는지가 중요합니다.
추상화 벽이 필요한 이유
- 구현을 간접적으로 감쌀 수 있어, 내부 구현을 바꾸더라도 상위 로직은 그대로 유지할 수 있습니다.
- 세부사항을 숨기기 때문에 더 단순하고 명확한 코드 작성이 가능합니다.
- 팀 간에 구체적인 내부 내용을 공유할 필요가 없어, 소통 비용을 줄여줍니다.
- 불필요한 세부 구현을 무시하고, 문제의 본질에 집중할 수 있게 합니다.
작은 인터페이스 (Minimal Interface)
작은 인터페이스란
- 작은 인터페이스란 추상화 벽에 꼭 필요한 기능만 제공하고, 불필요한 기능은 최소화하는 원칙입니다.
- 하위 계층의 인터페이스가 커질수록 관리가 복잡해지고 유지보수성도 떨어지기 때문에, 단순하고 간결한 인터페이스가 바람직합니다.
예제1: 제품을 많이 담은 사람이 시계를 구입하면 10% 할인
1. 추상화 벽에 만들기
tsx
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. 추상화 벽 위에 만들기
tsx
function getsWatchDiscount(cart) {
var total = calcTotal(cart); // 하위 계층의 함수 사용
var hasWatch = isInCart("watch"); // 하위 계층의 함수 사용
return total > 100 && hasWatch;
}
- 해시 데이터에 직접 접근하지 않습니다.
- 추상화 벽에 있는 함수를 활용해 장바구니를 다룹니다.
결과: 추상화 벽 위에 만들기
- 이 코드는 마케팅 로직이므로, 반복문 같은 세부 구현 대신 추상화된 함수를 사용하는 것이 적합합니다.
- 추상화 벽에 새로운 함수가 추가된다는 것은 계약이 늘어난 것과 같아, 그만큼 관리해야 할 범위도 커집니다.
- 따라서 불필요하게 하위 계층에 함수를 늘리기보다, 상위 계층에서 조합해 해결하는 것이 작은 인터페이스 패턴의 핵심입니다.
예제2: 장바구니에 제품을 담을때 로그 남기기
ts
logAddToCart(global_user_id, item);
1. add_item
에 추가하기
tsx
function add_item(cart, item) {
logAddToCart(global_user_id, item);
return objectSet(cart, item.name, item);
}
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();
}
}
add_item
안에 로그를 추가하면, 단순히 아이템을 담을 때뿐 아니라update_shipping_icons
실행 시에도 로그가 남게 됩니다.- 즉, 제품이 표시될 때마다 불필요하게 로그가 발생할 수 있습니다.
logAddToCart는
액션입니다.add_item
는 계산이었으나logAddToCart
를 호출하면서 액션으로 바뀝니다.add_item
를 호출하는 모든 함수가 액션이 되고, 액션이 전역으로 퍼지게 됩니다.
2. add_item_to_cart
에 추가하기
tsx
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();
}
- 장바구니 담기 동작을 담당하는 핸들러 함수
add_item_to_cart
안에서 로그를 남기는 것이 더 적절합니다. - 이렇게 하면 로그는 오직 실제 장바구니에 제품을 담을 때만 발생하며, 계산 함수(
add_item
)는 액션으로 오염되지 않고 순수성을 유지할 수 있습니다.
작은 인터페이스와 추상화 벽
- 추상화 벽은 곧 인터페이스입니다.
- 이 벽이 커질수록 수정해야 할 코드가 많아지고, 협업 비용도 함께 증가합니다.
- 따라서 인터페이스는 가능한 한 작고 단순하게 유지하는 것이 좋습니다.
- 상위 계층의 함수를 만들 때는, 새로운 함수를 추가하기보다 현재 계층의 함수들을 조합해 구현하는 것이 작은 인터페이스를 지키는 방법입니다.
작은 인터페이스 원칙을 지키면, 코드 변경에 강한 구조를 만들 수 있습니다.
편리한 계층 (Comfortable Layer)
편리한 계층이란
- 설계 패턴은 끝없이 적용할 필요가 없습니다.
- 코드가 충분히 읽기 쉽고 유지보수하기 편하다면, 그 지점에서 멈추는 것이 가장 좋습니다.
- 반대로, 구체적인 세부사항을 과하게 알아야 한다거나 코드가 불필요하게 지저분해 보인다면, 그때 다시 패턴을 적용해 계층을 다듬으면 됩니다.
계층별 코드의 역할
- 가장 높은 계층의 코드는 다른 곳에서 호출하지 않으므로, 비교적 쉽게 변경할 수 있습니다.
- 가장 낮은 계층에는 시간이 지나도 변하지 않는 안정적인 코드가 위치해야 합니다.
- 예를 들어, 카피온 라이트(copy-on-write) 원칙은 가장 아래 계층에 적용합니다.
- 가장 낮은 계층은 여러 곳에서 공통으로 사용되기 때문에, 테스트가 중요합니다.
정리하기
- 추상화 벽을 두면 상위 계층은 하위 구현을 몰라도 되며, 내부 변경에도 유연하게 대응할 수 있습니다.
- 작은 인터페이스를 지키면 불필요한 복잡성을 줄이고, 코드의 단순성과 유지보수성을 높일 수 있습니다.
- 설계는 완벽을 추구하는 끝없는 과정이 아니라, 코드가 충분히 읽기 쉽고 편리하다고 느껴지는 지점에서 멈추면 됩니다.