Chapter 9

계층형 설계 II

  • 9.1 추상화 벽
  • 9.2 추상화 벽 사용 규칙
  • 9.3 작은 인터페이스 패턴
  • 9.4 편리한 계층 패턴
  • 9.5 호출 그래프로 알 수 있는 것

추상화 벽에는 필수적인 함수만 두고, 나머지는 벽 위에서 조합해야 해.

8장에서 계층형 설계의 첫 번째 패턴인 "직접 구현"을 배웠다면, 9장에서는 나머지 세 가지 패턴을 다뤄. 첫 번째는 **추상화 벽(abstraction barrier)**인데, 특정 계층의 구현 세부사항을 완전히 숨기는 인터페이스야. 벽 위쪽의 코드는 벽 아래쪽이 어떻게 구현되어 있는지 전혀 모르거든. 장바구니를 배열로 저장하든 객체로 저장하든, 벽 위의 함수들은 신경 쓸 필요가 없어.

// 추상화 벽 아래 — 구현 세부사항
function add_item(cart, item) {
  return Object.assign({}, cart, {[item.name]: item});
}

function is_in_cart(cart, name) {
  return cart.hasOwnProperty(name);
}

// 추상화 벽 위 — 비즈니스 로직
function add_item_to_cart(name, price) {
  var item = make_item(name, price);
  cart = add_item(cart, item);  // 내부 구현 모름
  update_dom();
}

벽 위의 add_item_to_cart()는 장바구니가 배열인지 객체인지 관심이 없어. add_item()이라는 인터페이스만 알면 돼. 이게 왜 좋으냐면 — 나중에 장바구니 데이터 구조를 배열에서 해시맵으로 바꿔야 할 때, 벽 아래쪽만 수정하면 되기 때문이야. 벽 위의 코드는 하나도 안 건드려도 돼.

추상화 벽이 제대로 작동하려면 규칙이 있어. 벽 위의 코드는 벽 아래의 데이터 구조를 직접 사용하면 안 돼 — cart[0].price 같은 식으로 내부 구조에 접근하는 순간 벽이 무너지거든. 벽의 함수들은 완전한 인터페이스를 제공해야 하고, 벽은 팀 간 협업 경계로도 좋아("장바구니 팀은 벽 아래, 마케팅 팀은 벽 위"처럼 역할 분리 가능). 추상화 벽은 구현을 나중에 바꿀 가능성이 높을 때, 코드의 특정 부분이 자주 변경될 때, 팀 간 경계를 명확히 하고 싶을 때, 라이브러리나 외부 서비스를 감싸고 싶을 때 쓰면 좋은데, 반대로 모든 곳에 추상화 벽을 세우면 오버엔지니어링이야. 변경 가능성이 높은 곳에만 선별적으로 사용해야 해.

두 번째 패턴은 **작은 인터페이스(minimal interface)**야. 새로운 기능을 추가할 때 기존 추상화 벽에 함수를 추가하지 말고, 벽 위에서 기존 함수를 조합하라는 거지.

// 나쁜 방법 — 벽에 함수 추가 (인터페이스가 커짐)
function discount_all_items(cart, discount_rate) {
  // 내부 구현에 접근해서 할인 적용
}

// 좋은 방법 — 벽 위에서 기존 함수 조합
function discount_all(cart, discount_rate) {
  var names = get_item_names(cart);         // 기존 벽 함수
  for (var i = 0; i < names.length; i++) {
    var price = get_item_price(cart, names[i]); // 기존 벽 함수
    var new_price = price * (1 - discount_rate);
    cart = set_item_price(cart, names[i], new_price); // 기존 벽 함수
  }
  return cart;
}

인터페이스가 커지면 유지보수 비용이 늘어나거든. 추상화 벽의 함수가 많을수록, 내부 구현을 바꿀 때 수정할 함수도 많아지니까.

세 번째 패턴은 **편리한 계층(comfortable layer)**인데, 좀 현실적인 조언이야 — 계층을 나눌 때 작업하기 편한 수준에서 멈추라는 거야. 계층형 설계의 이론적 완벽함을 추구하다가 오히려 생산성이 떨어지면 본말전도잖아. "지금 이 코드가 작업하기 불편하면 계층을 추가하고, 편하면 그대로 두자." 이게 편리한 계층의 요지야. 과도한 추상화는 오히려 코드를 읽기 어렵게 만들거든.

호출 그래프를 그리면 설계에 대한 유용한 정보를 여러 가지 읽어낼 수 있어. 유지보수성 면에서 그래프에서 위쪽에 있는 함수일수록 자주 바뀌고, 아래쪽에 있는 함수는 안정적이야. 테스트 가능성 면에서 아래쪽에 있는 함수일수록 테스트하기 쉬워 — 의존성이 적으니까. 재사용성 면에서도 아래쪽 함수가 더 재사용하기 쉬워. 정리하면 — 함수를 그래프에서 아래쪽으로 내릴수록(더 일반적으로 만들수록) 테스트하기 쉽고, 재사용하기 쉽고, 변경에 안정적이 돼.


정리

9장 읽고 기억할 거 세 가지:

  1. 추상화 벽 = 구현 세부사항을 완전히 숨기는 계층. 벽 위는 벽 아래의 데이터 구조를 절대 직접 쓰지 않는다
  2. 작은 인터페이스 = 벽에 함수를 추가하지 말고 기존 함수를 조합. 인터페이스가 커지면 유지보수 비용이 늘어난다
  3. 호출 그래프 아래쪽일수록 안정적, 테스트 쉬움, 재사용 쉬움. 함수를 가능한 아래로 내리는 게 좋은 설계