Ch08-09 계층형 설계

·

5 min read

계층형 설계 stratified design

소프트웨어를 계층으로 구성하는 기술. 각 계층에 있는 함수는 바로 아래 계층의 함수를 이용해 정의

  • 장바구니 기능

    • 비즈니스 규칙, 특정 서비스만의 규칙: getFreeShipping(), cartTax()

    • 장바구니를 위한 동작, 도메인 영역에서 공통적으로 적용되는 규칙: removeItemByName(), calcTotal(), addItem(), setPriceByName()

    • copy-on-write: removeItems(), addElementLast()

    • 언어에서 지원하는 기능: array.slice()

각 계층을 정확히 구분하기는 어렵지만, 계층을 잘 구분하려면 구분하기 위한 다양한 변수를 찾고, 찾은 것을 가지고 어떻게 해야하는지 알아야 한다

계층형 설계 감각 키우기

입력

함수 본문

  • 길이

  • 복잡성

  • 구체화 단계

  • 함수 호출

  • 프로그래밍 언어의 기능 사용

계층 구조

  • 화살표 길이

  • 응집도

  • 구체화 단계

함수 시그니처

  • 함수명

  • 인자 이름

  • 인자 값

  • 리턴 값

출력

조직화

  • 새로운 함수를 어디에 놓을지 결정

  • 함수를 다른 곳으로 이동

구현

  • 구현 바꾸기

  • 함수 추출하기

  • 데이터 구조 바꾸기

변경

  • 새 코드를 작성할 곳 선택하기

  • 적절한 수준의 구체화 단계 결정하기

계층형 설계 패턴

패턴 1: 직접 구현

  • 한 단계 구체화 수준에 관한 문제만 해결

    • 직접 구현된 함수를 읽을 때, 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절한 구체화 수준에서 해결해야 한다

    • 너무 구체적이라면 코드에서 나는 냄새임

  • 함수가 더 구체적인 내용을 다루지 않도록 함수를 일반적인 함수로 빼내기

    • 일반적인 함수가 많을수록 재사용성이 높아짐

패턴 2: 추상화 벽 abstraction barrier

  • 중요한 세부 구현을 감추고 인터페이스를 제공

    • 추상화 벽 위 계층에 있는 함수들은 그 아래 계층 함수들이 사용하는 데이터 구조를 몰라도 됨

      • 마케팅 관련 코드는 장바구니가 어떻게 구현되어 있는지 알 필요가 없다
    • 인터페이스를 활용하면 고차원으로 생각할 수 있고,

    • 고수준의 추상화 단계만 생각하면 되기 때문에 두뇌 용량의 한계를 극복할 수 있음

  • 변경사항이 있을때, 동일한 추상화 레벨에 있는 함수들만 변경하면 됨

    • 👉 추상화 벽을 작게 만들어야 하는 이유 중 하나; 구현이 변경되었을 때 고쳐야 할 것이 적음

활용

  • 쉽게 구현을 바꾸기 위해

    • 프로토타이핑, API 임시 데이터 등 구현에 대한 확신이 없는 경우

    • 다만 만약을 대비해 코드를 만드는 것은 좋지 않은 습관이므로 주의

      • 바뀌지 않을지도 모르는 코드를 언젠가 쉽게 바꿀 수 있게 만들려는 함정에 빠지지 말자

      • 대부분 데이터 구조는 바뀌지 않는다

      • 추상화 벽은 코드를 쉽게 고치려고 사용하는 것이 아니라, 신경 쓰지 않아도 되는 것을 다루는 것이 핵심

  • 코드를 읽고 쓰기 쉽게 만들기 위해

    • 때로는 구체적인 것이 버그를 만든다

      • 반복문 off-by-one 에러 등
  • 팀 간 조율해야 할 것을 줄이기 위해

  • 주어진 문제에 집중하기 위해

패턴 3: 작은 인터페이스 minimal interface

  • 새로운 기능을 추가할 때, 하위 계층에 기능을 추가하거나 고치는 것보다 상위 계층에 만드는 것

    • 시스템이 커질수록 비즈니스 개념을 나타내는 중요한 인터페이스는 작고 강력한 동작으로 구성하는 것이 좋다
  • 이상적인 계층은 최소한의 필요한 함수만 갖고 있고, 함수는 바뀌지 않아야 함

    • 현실적으로는 불가능하지만, 이 목표에 가려고 노력해야
  • 함수 목적에 맞는 계층이 어디인지 찾는 감각을 기르는 것이 가장 중요

    • 하려는 일이 적은 함수로 잘할 수 있는가?

    • 목적에 맞게 바꾸려고 하고 있는가?

패턴 4: 편리한 계층 comfortable layers

  • 코드와 그 추상화 계층은 작업할 때 편리해야 하고, 비즈니스 문제를 잘 풀 수 있어야 함

    • 그냥 좋아서 계층을 추가하면 안된다

    • 강력한 추상화 계층은 만들기 어렵고, 시간이 지나면 크게 도움이 되지 않는다고 느낄 것

  • 언제 패턴을 적용하고 언제 멈춰야 하는가?

    • 지금 편리한가? 그렇다면 설계는 조금 멈춰도 된다

    • 구체적인 것을 너무 많이 알아야 하거나, 코드가 지저분하게 느껴지는가? 그렇다면 패턴을 적용하라

ex. 넥타이 하나를 사면 무료로 넥타이 클립을 하나 주는 코드

유지보수 하기 어려운 코드

function freeTieClip(cart) {
  let hasTie = false;
  let hasTieClip = false;
  for (let i = 0; i < cart.length; i++) {
    const item = cart[i];
    if (item.name === 'tie') hasTie = true;
    if (item.name === 'tie clip') hasTieClip = true;
  }

  if (hasTie && !hasTieClip) {
    const tieClip = makeItem('tie clip', 0);
    return addItem(cart, tieClip);
  }

  return cart;
}
  • '직접 구현' 패턴을 따르지 않았기 때문

    • 마케팅 캠페인에 관련된 함수가 장바구니가 배열이라는 사실을 알 필요가 없다

    • 장바구니 배열을 돌다가 off-by-one 에러가 생기면 실패할 수 있다

코드 개선하기

직접 구현 패턴 적용하기

저수준 코드 추출하기
  • 장바구니 안에 제품이 있는지 확인하는 함수가 있다면, 저수준 반복문을 직접 쓰지 않아도 됨
호출 그래프를 만들어 함수 호출 시각화하기
  • 함수에서 사용하는 다른 함수, 언어 기능을 호출 그래프로 시각화

  • 해당 함수/기능들의 추상화 수준이 같은지 확인

    • 한 함수에서 서로 다른 추상화 단계를 사용하면 코드가 명확하지 않아 읽기 어렵다

    • 다른 함수: makeItem, addItem

    • 언어 기능: array index, for loop

  • 저수준 코드를 함수로 분리

    • isInCart
  • 개선된 함수 freeTieClip에서는 장바구니가 배열인지 몰라도 됨 => 내부에서 사용하는 함수들이 모두 비슷한 계층에 있다는 것

  • 모든 함수가 그래프에 있어야 하고, 화살표는 옆이나 위가 아닌 아래로 향해야 함

Screenshot-2023-01-06-at-9-35-54-PM

  • 한 함수의 모든 화살표는 같은 길이를 가져야 한다

    • 화살표가 복잡한 이유는 코드가 정돈되어 있지 않기 때문

    • 한 함수에서 여러 계층을 사용하지 않도록 정리해야 함

    • 경우에 따라 개선을 하더라도 화살표 길이가 여전히 동일하지 않을 수 있지만, 화살표 수를 줄이거나 길이가 좀더 줄어든다면 괜찮다

비기능적 요구사항 nonfunctional requirements

  • 호출 그래프를 통해 알 수 있는 것

  • 소프트웨어 설계를 하는 중요한 이유

유지보수성 maintainability

자주 바뀌는 코드는 가능한 위쪽에 있어야

  • 다른 코드에 영향을 주지 않는 코드

  • 자주 바뀌므로 적게 유지하는 것이 좋다

테스트성 testability

시간이 지나도 변하지 않는 코드는 가장 아래 계층에 있어야

  • copy-on-write 함수 등 한번 잘 만들어 두면 바꿀 일이 없음

  • 테스트가 중요

    • 위에 있는 코드보다 더 안정적이고 오래 가야 하므로

    • 잘 만든 코드, 테스트는 자주 고칠 필요도 없음

재사용성 reusability

낮은 수준으로 함수를 빼내면 재사용성이 더 높아짐

  • 아래에 있는 코드가 재사용하기 더 좋음