계층형 설계 I
코드가 복잡해지는 가장 흔한 이유는 서로 다른 추상화 수준의 로직이 한 함수 안에 섞여 있기 때문입니다.
이럴 때 해결책이 되는 것이 바로 계층형 설계(Stratified Design) 입니다.
계층형 설계는 코드를 여러 계층으로 나누고, 각 계층이 바로 아래 계층에만 의존하도록 만드는 방식입니다.
이번 글에서는 그중 첫 번째 패턴인 직접 구현(Straightforward Implementation) 에 대해 다룹니다.
스터디 회차: 7회차 (2025년 8월 26일)
계층형 설계 (Stratified Design)
계층형 설계란
- 계층형 설계는 소프트웨어를 여러 계층으로 나누고, 각 계층이 바로 아래 계층에만 의존하도록 구성하는 방법입니다.
- 이를 잘하기 위해서는 함수 본문, 계층 구조, 함수 시그니처 등 다양한 요소를 고려하고, 새로운 함수를 어느 위치에 둘지, 어떻게 구현할지를 끊임없이 결정해야 합니다.
계층형 설계 패턴
- 직접 구현: 문제를 적절한 추상화 수준에서 해결하기
- 추상화 벽: 세부 구현을 감추고 인터페이스 제공하기
- 작은 인터페이스: 꼭 필요한 최소한의 인터페이스 유지하기
- 편리한 계층: 작업하기에 편리한 추상화 계층 제공하기
직접 구현 패턴 (Straightforward implementation)
직접 구현 패턴이란
- 코드를 설계할 때는 가능한 한 단순하게 구현하는 것이 좋습니다.
- 특정 기능을 작성할 때는 그 기능을 해당 계층에서 직접 해결해야 하며, 불필요하게 다른 추상화 단계로 넘기거나 감출 필요가 없습니다.
- 또한 기능을 작은 단위의 함수로 나누어 명확하고 단순한 구조를 유지하는 것이 바람직합니다.
즉, 직접 구현 패턴은 문제를 우회하거나 숨기지 않고 현재 계층에서 곧바로 해결하는 방식을 의미합니다.
예제: 넥타이를 사면 넥타이 클립 무료 제공
tsx
function freeTieClip(cart) {
var hasTie = false;
var hasTieClip = false;
for (var i = 0; i < cart.length; i++) {
var item = cart[i];
if (item.name === "tie") hasTie = true;
if (item.name === "tie clip") hasTieClip = true;
}
if (hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
문제점
freeTieClip
함수가 장바구니 내부 구조(배열, 인덱스 탐색 등)를 직접 다룹니다.- 그 결과, 비즈니스 로직과 세부 구현이 섞여 가독성이 떨어집니다.
개선하기
tsx
function freeTieClip(cart) {
var hasTie = isInCart(cart, "tie");
var hasTieClip = isInCart(cart, "tie clip");
if (hasTie && !hasTieClip) {
var tieClip = make_item("tie clip", 0);
return add_item(cart, tieClip);
}
return cart;
}
function isInCart(cart, name) {
for (var i = 0; i < cart.length; i++) {
if (cart[i].name === name) return true;
}
return false;
}
- 세부구현 부분을
isInCart
함수로 분리하여 추상화 수준을 비슷하게 맞춥니다. freeTieClip
안에 있는isInCart
,make_item
,add_item
들이 비슷한 추상화를 가집니다.
호출 그래프로 알아보기
개선 전
freeTieClip
은 반복문, 배열 인덱스 같은 저수준 동작과 비즈니스 로직을 동시에 다룹니다.- 한 함수가 서로 다른 추상화 단계를 섞어 사용하므로 가독성이 떨어집니다.
개선 후
- 모두 비슷한 추상화 수준의 함수들이어서 호출 구조가 단순해졌습니다.
freeTieClip
은 이제 장바구니가 배열인지조차 알 필요가 없어졌습니다.
TIP
계층형 설계는 정답이 있는 기법이 아닙니다.
여러 관점에서 시도하고 경험적으로 다듬어가는 과정입니다.
계층 결정하기
예제: remove_item_by_name
을 계층 구조에 추가하기
freeTieClip
은 마케팅 캠페인에 대한 로직이고,remove_item_by_name
는 마케팅과 관련없는 일반적인 동작이므로freeTieClip
보다 낮은 계층에 위치합니다.isInCart
와remove_item_by_name
는 같은 박스를 가리키고 있습니다.- 같은 박스를 가리킨다는 것은 동일한 계층에서 다뤄져야 한다는 신호입니다.
- 따라서
remove_item_by_name
은isInCart
와 동일한 계층에 두는 것이 적절합니다.
TIP
함수 이름은 함수가 어느 곳에 위치할지 결정하기 위한 정보로 쓸 수 있습니다.
더 좋은 계층 구조 만들기
같은 계층에 있는 함수는 같은 목적(같은 구체화 수준)을 가져야 합니다.
개선 전
- 직접 구현 패턴을 사용하면 모든 화살표가 같은 길이를 가져야합니다.
- 다양한 계층을 넘나드는 것을 통해 같은 구체화 수준이 아니라는 것을 알 수 있습니다.
개선 후
- 가장 일반적인 방법은 중간에 함수를 두는 것입니다.
tsx
function remove_item_by_name(cart, name) {
var idx = indexOfItem(cart, name);
if (idx !== null) return removeItems(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;
}
indexOfItem
를 추가하여 배열의 세부 구현은indexOfItem
에서 다루고,remove_item_by_name
함수 내에서 비슷한 계층의 함수를 호출하도록 개선합니다.
정확히는 indexOfItem
함수가 removeItems
함수보다 조금 더 위에 위치합니다
ts
function indexOfItem(cart, name) {
for (var i = 0; i < cart.length; i++) {
if (cart[i].name === name) return i;
}
return null;
}
function removeItems(array, idx, count) {
var copy = array.slice();
copy.splice(idx, count);
return copy;
}
indexOfItem
함수는 배열에 항목이 name 속성이 있다는 것을 알아야하지만removeItems
함수는 배열에 들어있는 항목이 어떻게 생겼는지 몰라도 됩니다.- 그래서
removeItems
함수는indexOfItem
함수보다 더 일반적이고indexOfItem
함수보다 조금 더 낮은 계층에 있습니다.
정리하기
- 한 함수 안에 서로 다른 추상화 수준이 섞이면 읽기 어렵습니다.
- 직접 구현 패턴은 문제를 우회하거나 숨기지 않고 현재 계층에서 곧바로 해결하는 방식을 의미합니다.
- 개선 방법 중 하나는, 구체적인 로직을 별도의 일반화된 함수로 분리하는 것입니다.