Skip to content

계층형 설계 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) 원칙은 가장 아래 계층에 적용합니다.
  • 가장 낮은 계층은 여러 곳에서 공통으로 사용되기 때문에, 테스트가 중요합니다.

정리하기

  • 추상화 벽을 두면 상위 계층은 하위 구현을 몰라도 되며, 내부 변경에도 유연하게 대응할 수 있습니다.
  • 작은 인터페이스를 지키면 불필요한 복잡성을 줄이고, 코드의 단순성과 유지보수성을 높일 수 있습니다.
  • 설계는 완벽을 추구하는 끝없는 과정이 아니라, 코드가 충분히 읽기 쉽고 편리하다고 느껴지는 지점에서 멈추면 됩니다.