본문으로 건너뛰기

08 계층형 설계 1

이번 장에서 살펴볼 내용
  • 소프트웨어 설계에 대한 실용적인 정의를 소개합니다.
  • 계층형 설계를 이해하고 어떤 도움이 되는지 알아봅니다.
  • 깨끗한 코드를 만들기 위해 함수를 추출하는 방법을 배웁니다.
  • 계층을 나눠서 소프트웨어를 설계하면 왜 더 나은 생각을 할 수 있는지 알아봅니다.

소프트웨어 설계란 무엇입니까?

소프트웨어 설계

코드를 만들고, 테스트하고, 유지보수하기 쉬운 프로그래밍 방법을 선택하기 위해 미적 감각을 사용하는 것

이 장에서는 계층형 설계를 사용해 소프트웨어 설계를 위한 미적 감각을 키워볼 것입니다.

계층형 설계란 무엇인가요?

계층형 설계는 소프트웨어를 계층으로 구성하는 기술입니다. 각 계층에 있는 함수는 바로 아래 계층에 있는 함수를 이용해 정의합니다. 계층을 잘 구분하려면 구분하기 위한 다양한 변수를 찾고 찾은 것을 가지고 어떻게 해야 하는지 알아야 합니다. '가장 좋은 설계'를 위한 절대 공식과 그 공식을 만드는 변수는 많이 있지만 복잡하게 섞여 있어 찾기 어렵습니다.

계층형 설계 패턴

패턴 1: 직접 구현

직접 구현은 계층형 설계 구조를 만드는 데 도움이 됩니다. 직접 구현된 함수를 읽을 때, 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절한 구체화 수준에서 해결해야 합니다. 만약 너무 구체적이라면 코드에서 나는 냄새입니다.

패턴 2: 추상화 벽

호출 그래프에 어떤 계층은 중요한 세부 구현을 감추고 인터페이스를 제공합니다. 인터페이스를 사용하여 코드를 만들면 높은 차원으로 생각할 수 있습니다.

패턴 3: 작은 인터페이스

시스템이 커질수록 비즈니스 개념을 나타내는 중요한 인터페이스는 작고 강력한 동작으로 구성하는 것이 좋습니다. 다른 동작도 직간접적으로 최소한의 인터페이스를 유지하면서 정의해야 합니다.

패턴 4: 편리한 계층

계층형 설계 패턴과 실천 방법은 개발자의 요구를 만족시키면서 비즈니스 문제를 잘 풀 수 있어야 합니다. 소프트웨어를 더 빠르고 고품질로 제공하는 데 도움이 되는 계층에 시간을 투자해야 합니다. 그냥 좋아서 계층을 추가하면 안 됩니다. 코드와 그 코드가 속한 추상화 계층은 작업할 때 편리해야 합니다.

패턴 1: 직접 구현

계층 구조는 아무리 강력한 기능을 하는 함수가 있더라도 복잡하지 않게 함수를 표현해야 합니다.

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() 함수가 알아야 할 필요가 없는 구체적인 내용을 담고 있습니다. 마케팅 캠페인에 관련된 함수가 장바구니가 배열이라는 사실을 알아야 할까요?

제품이 있는지 확인하는 함수가 있다면 설계를 개선할 수 있습니다.

장바구니가 해야 할 동작을 모두 정리해보니 개선할 수 있는 부분을 발견했습니다. freeTieClip() 함수에 직접 구현 패턴을 적용할 수 있을 것 같습니다.

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);
}
}

function isInCart(cart, name) {
for (var i = 0; i < cart.length; i++) {
if (cart[i].name === name) {
return true;
}
}
return false;
}

개선한 함수는 짧고 명확합니다. 또 모두 비슷한 구체화 수준에서 작동하고 있기 때문에 읽기 쉽습니다. 직접 구현 패턴을 사용하면 모든 화살표가 같은 길이를 가져야 합니다.

같은 계층에 있는 함수는 같은 목적을 가져야 합니다.

다이어그램은 명확하고 모호한 것이 없는 여섯 개의 계층으로 되어있습니다. 함수를 어떤 계층에 놓을지 선택하는 과정은 복잡합니다. 그래도 계층이 서로 구분되는 목적이 있다면, 함수가 위치할 계층을 선택하는 데 좋은 정보로 사용할 수 있습니다. 계층의 목적은 각 계층에 있는 함수의 목적과 같습니다.

img

각 계층은 추상화 수준이 다릅니다. 그래서 어떤 계층에 있는 함수를 읽거나 고칠 때 낮은 수준의 구체적인 내용은 신경 쓰지 않아도 됩니다.

다이어그램은 함수가 호출하는 것을 있는 그대로 표현한 것이기 때문에 함수를 어떤 계층에 놓을지 바로 알 수 있습니다.

3단계 줌 레벨

함수 줌 레벨을 사용하면 함수 하나가 가진 화살표를 비교할 수 있습니다.

img

함수 하나를 살펴봤을 뿐인데 서로 다른 계층의 동작을 사용하는 것을 확인할 수 있습니다. 이것은 직접 구현 패턴에 맞지 않습니다.

직접 구현 패턴을 사용하면 remove_item_by_name()가 모두 같은 길이의 화살표를 가져야 합니다.

가장 일반적인 방법은 중간에 함수를 두는 것입니다. 언어 기능을 사용하는 긴 화살표를 줄여야 합니다. removeItems() 함수와 같은 계층에 반복문과 배열 인덱스 참조를 담당하는 함수를 만들면 모든 화살표 길이가 같아질 것입니다.

img

직접 구현 패턴 리뷰

직접 구현한 코드는 한 단계의 구체화 수준에 관한 문제만 해결합니다.

좋은 설계를 고민하지 않고 만든 코드는 읽거나 고치기 어렵습니다. 왜 어려울까요? 코드가 서로 다른 구체화 단계에 있다면 읽기 어렵습니다. 코드를 읽을 때 이해해야 할 것이 많이 있는데 구체화 단계가 다르다면 이해하기가 더 어렵습니다.

걔층 설계는 특정 구체화 단계에 집중할 수 있게 도와줍니다.

코드에 있는 다양한 단서를 통해 구체화 수준에 집중하다 보면 설계 감각을 키울 수 있고 코드를 필요에 알맞게 바꿀 수 있습니다.

호출 그래프는 구체화 단계에 대한 풍부한 단서를 보여줍니다.

코드에는 설계를 개선하기 위한 단서가 많이 있습니다. 하지만 큰 그림으로 한 번에 보기에는 너무 많은 정보가 있습니다. 호출 그래프는 함수가 서로 어떻게 연결되어 있는지 보여줍니다.

함수를 추출하면 더 일반적인 함수로 만들 수 있습니다.

함수에 직접 구현 패턴을 적용하는 방법의 하나는 함수가 더 구체적인 내용을 다루지 않도록 함수를 일반적인 함수를 빼내는 것입니다. 일반적인 함수는 보통 구체적인 내용을 하나만 다루기 때문에 테스트하기 쉽습니다. 명확한 코드와 알맞은 이름을 가진 함수는 더 읽기 쉽습니다.

일반적인 함수가 많을수록 재사용하기 좋습니다.

함수로 빼내면 재사용할 수 있는 곳이 보입니다. '중복 코드'를 찾기 위해 함수를 빼내는 것과는 다릅니다. 일반적인 함수는 구체적인 함수보다 더 많은 곳에서 쓸 수 있습니다.

복잡성을 감추지 않습니다.

직접 구현 패턴을 적용한 코드처럼 보이게 만드는 것은 쉽습니다. 명확하지 않은 코드를 감추기 위해 'helper function'을 만들면 됩니다. 하지만 이렇게 하는 것은 계층형 설계가 아닙니다. 계층형 설계에서 모든 계층은 바로 아래 계층에 의존해야 합니다. 복잡한 코드를 같은 계층으로 옮기면 안됩니다. 더 낮은 구체화 수준을 가진 일반적인 함수를 만들어 소프트웨어에 직접 구현 패턴을 적용해야 합니다.

요점 정리

  • 계층형 설꼐는 코드를 추상화 계층으로 구성합니다. 각 계층을 볼 때 다른 계층에 구체적인 내용을 몰라도 됩니다.
  • 문제 해결을 위한 함수를 구현할 때 어떤 구체화 단계로 쓸지 결정하는 것이 중요합니다. 그래야 함수가 어떤 계층에 속할지 알 수 있습니다.
  • 함수 이름은 의도를 알려줍니다. 비슷한 목적의 이름을 가진 함수를 함께 묶을 수 있습니다.
  • 호출 그래프로 구현이 직접적이지 않다는 것을 알 수 있습니다. 함수를 호출하는 화살표가 다양한 길이를 가지고 있다면 직접 구현되어 있지 않다는 신호입니다.