코드베이스 설계
깊은 모듈을 설계하세요: 작은 인터페이스 뒤에 많은 동작이 숨겨져 있고, 호출자와 테스트가 만나는 접합면(seam)에 배치됩니다. 목표는 호출자에게 레버리지(leverage)를, 유지보수자에게 지역성(locality)을, 그리고 리팩터링에도 살아남는 테스트를 제공하는 것입니다.
규칙
- 깊이는 인터페이스의 속성일 뿐 구현의 속성이 아닙니다. 깊은 모듈은 작고 교체 가능한 부품들로 구성될 수 있습니다. 단, 그 부품들이 인터페이스의 일부가 아니어야 합니다. 깊은 모듈은 자체 테스트를 위한 내부 접합면과 인터페이스에 있는 외부 접합면을 모두 가질 수 있습니다.
- 삭제 테스트를 수행하세요. 모듈을 삭제한다고 가정해보세요. 복잡성이 사라진다면 단순 통과(pass-through) 모듈입니다. 복잡성이 호출자들 사이에 다시 나타난다면 그 모듈은 존재 가치가 있는 것입니다.
- 인터페이스가 테스트 표면입니다. 호출자와 테스트는 동일한 접합면을 넘나듭니다. 인터페이스 너머를 테스트해야 한다면 모듈의 형태가 잘못된 것입니다.
- 어댑터 하나는 가상의 접합면; 두 개가 실제로 만듭니다. 접합면은 실제로 무언가가 변하는 곳에만 추가하세요. 보통은 프로덕션과 테스트입니다.
- 인터페이스를 축소하세요. 더 적은 메서드, 더 단순한 매개변수, 더 많은 복잡성을 내부에 숨기세요.
- 의존성을 생성하지 말고 주입받으세요. 함수 내부에서 결제 게이트웨이를 생성하지 말고 전달받으세요.
- 설계가 허용하는 곳에서는 부수 효과 대신 결과를 반환하세요.
용어
다음 용어들을 정확히 사용하세요. "컴포넌트", "서비스", "API", "경계"로 대체하지 마세요.
- 모듈: 인터페이스와 구현을 가진 모든 것, 모든 규모(함수, 클래스, 패키지, 계층 간 조각).
- 인터페이스: 호출자가 모듈을 올바르게 사용하기 위해 알아야 하는 모든 것: 타입, 불변 조건, 순서, 오류 모드, 필수 구성, 성능. 단순한 시그니처가 아닙니다.
- 구현: 모듈 내부의 코드. 작은 어댑터가 큰 구현(PostgreSQL 저장소)을 가질 수도 있고, 큰 어댑터가 작은 구현(인메모리 가짜 객체)을 가질 수도 있습니다.
- 깊이: 인터페이스에서의 레버리지. 즉, 호출자나 테스트가 배워야 하는 인터페이스 단위당 실행할 수 있는 동작의 양. 작은 인터페이스 뒤에 많은 동작이 숨겨져 있으면 깊은 모듈이고, 인터페이스가 구현만큼 복잡하면 얕은 모듈입니다.
- 접합면(Seam) (Michael Feathers): 해당 위치 자체를 수정하지 않고도 동작을 변경할 수 있는 곳. 모듈의 인터페이스가 존재하는 곳입니다. 접합면을 선택하는 것은 그 자체로 별도의 결정입니다.
- 어댑터: 접합면에서 인터페이스를 채우는 구체적인 것. 역할의 이름이지 내용물이 아닙니다.
- 레버리지: 호출자가 깊이로부터 얻는 이점. 하나의 구현이 많은 호출 지점과 테스트에서 보답합니다.
- 지역성: 유지보수자가 깊이로부터 얻는 이점. 변경, 버그, 지식, 검증이 한 곳에 집중됩니다.
거부된 프레이밍: 깊이를 구현 라인 수와 인터페이스 라인 수의 비율로 보는 것(패딩을 조장하기 때문); "인터페이스"를 단지 타입 키워드나 public 메서드로만 보는 것(너무 좁음); "경계"(DDD의 제한된 컨텍스트와 충돌하기 때문).
프로젝트에 CONTEXT.md 용어집이 있다면, 이 용어들과 함께 해당 도메인 용어를 사용하세요.
더 깊이 들어가기
- 의존성 유형별로 얕은 모듈 클러스터를 깊게 만들려면 DEEPENING.md를 읽으세요.
- 근본적으로 다른 여러 인터페이스를 비교하려면 DESIGN-IT-TWICE.md를 읽으세요. 이 문서는 세 개 이상의 하위 에이전트를 시작하므로, 사용자가 요청할 때만 사용하세요.