Ch08-09 계층형 설계
계층형 설계 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
에서는 장바구니가 배열인지 몰라도 됨 => 내부에서 사용하는 함수들이 모두 비슷한 계층에 있다는 것모든 함수가 그래프에 있어야 하고, 화살표는 옆이나 위가 아닌 아래로 향해야 함
한 함수의 모든 화살표는 같은 길이를 가져야 한다
화살표가 복잡한 이유는 코드가 정돈되어 있지 않기 때문
한 함수에서 여러 계층을 사용하지 않도록 정리해야 함
경우에 따라 개선을 하더라도 화살표 길이가 여전히 동일하지 않을 수 있지만, 화살표 수를 줄이거나 길이가 좀더 줄어든다면 괜찮다
비기능적 요구사항 nonfunctional requirements
호출 그래프를 통해 알 수 있는 것
소프트웨어 설계를 하는 중요한 이유
유지보수성 maintainability
자주 바뀌는 코드는 가능한 위쪽에 있어야
다른 코드에 영향을 주지 않는 코드
자주 바뀌므로 적게 유지하는 것이 좋다
테스트성 testability
시간이 지나도 변하지 않는 코드는 가장 아래 계층에 있어야
copy-on-write 함수 등 한번 잘 만들어 두면 바꿀 일이 없음
테스트가 중요
위에 있는 코드보다 더 안정적이고 오래 가야 하므로
잘 만든 코드, 테스트는 자주 고칠 필요도 없음
재사용성 reusability
낮은 수준으로 함수를 빼내면 재사용성이 더 높아짐
- 아래에 있는 코드가 재사용하기 더 좋음