Chapter 6

모듈성, 응집성, 관심사 분리

  • 6.1 모듈성의 전형적인 특징
  • 6.2 설계는 언제나 중요하다
  • 6.3 TDD의 교훈: 테스트가 어렵다면 설계도 문제다
  • 6.4 TDD로 모듈성을 강화하자
  • 6.5 REST API로 모듈성을 강화하자
  • 6.6 배포 파이프라인으로 모듈성을 강화하자
  • 6.7 모듈성의 규모는 크고 작음이 없다
  • 6.8 고성과 개발 조직의 특징: 모듈형
  • 6.9 모듈성과 응집성: 설계의 기초
  • 6.10 응집성을 개선하기 위한 리팩토링 사례 하나
  • 6.11 DDD의 컨텍스트를 활용한 응집성 개선
  • 6.12 소프트웨어에서 '고성능'의 의미란
  • 6.13 결합도와 응집성 사이의 관계
  • 6.14 TDD로 응집성을 높이자
  • 6.15 응집성 있는 소프트웨어를 만들려면
  • 6.16 응집성이 부족할 때 치러야 할 대가
  • 6.17 개발 조직 관점에서 응집성의 중요성
  • 6.18 의존성 주입
  • 6.19 본질적인 복잡성과 우발적인 복잡성을 분리하자
  • 6.20 DDD는 중요하다: 경계 컨텍스트를 활용한 하향식 관심사 분리
  • 6.21 테스트하기 쉬운 코드 = 관심사가 분리된 코드
  • 6.22 육각형 아키텍처: 포트와 어댑터
  • 6.23 포트와 어댑터는 언제 채택해야 할까
  • 6.24 API가 단순한 함수 호출이 아닌 이유
  • 6.25 TDD를 이용한 상향식 관심사 분리

모듈성, 응집성, 관심사 분리 — 이 세 가지는 결국 하나의 질문으로 수렴해. "변경의 영향 범위를 어떻게 제한할 것인가?"

모듈성의 진짜 의미는 "코드를 파일로 나누는 것"이 아니야. 변경의 영향 범위를 제한하는 것이야. 파일이 100개로 나뉘어 있어도, 하나를 바꾸면 나머지 99개를 다 수정해야 한다면 그건 모듈성이 아니잖아. 진짜 모듈은 명확한 인터페이스를 갖고, 내부 구현을 숨기고, 독립적으로 교체 가능해야 해. 이 세 가지가 레고 블록이 레고 블록인 이유야 — 안의 구조를 몰라도 끼우고 빼는 게 자유롭잖아.

그래서 "일단 동작하게 만들고 나중에 리팩토링하자"는 접근이 위험한 거야. 나중에 모놀리식 코드를 모듈로 분리하는 건 처음부터 모듈로 만드는 것보다 10배는 어렵거든. 코드 간의 암묵적 의존성이 이미 뿌리를 내렸기 때문이야. 설계를 미룬다는 건 사실 "나쁜 설계를 선택한다"와 같은 말이야.

그러면 처음부터 좋은 모듈성을 어떻게 확보하느냐? Farley의 답은 TDD야. 테스트를 먼저 작성하면 "이 모듈이 외부에 뭘 제공해야 하지?"를 자연스럽게 먼저 생각하게 돼. 테스트가 곧 모듈의 첫 번째 사용자가 되는 셈이지. 그리고 재미있는 게, 테스트 작성이 어려운 코드는 거의 100% 설계가 나쁜 코드야. DB 연결이랑 비즈니스 로직이랑 UI가 한 클래스에 뒤엉켜 있으면, 비즈니스 로직만 테스트하고 싶어도 전부 띄워야 하잖아. 테스트의 어려움은 모듈성이 없다는 신호인 거야.

이 원칙은 규모에 상관없이 동일하게 적용돼. 함수 하나가 너무 많은 일을 하면 안 되는 것처럼, 마이크로서비스 하나가 너무 많은 책임을 지면 안 돼. REST API가 서비스 간 경계를 HTTP로 강제하든, 배포 파이프라인이 각 모듈의 독립적 릴리스를 검증하든 — 결국 같은 질문이야. "이걸 독립적으로 바꿀 수 있어?"

DORA 연구가 이걸 데이터로 증명했어. 고성과 조직은 공통적으로 느슨하게 결합된 아키텍처를 가지고 있어. 그리고 콘웨이의 법칙에 따라, 모듈성 있는 소프트웨어를 만들려면 팀 구조도 모듈화되어야 해. 결국 모듈성은 코드의 문제가 아니라, 소프트웨어를 만드는 조직 전체의 효율성 문제야.

모듈성이 "잘 나누는 것"이라면, **응집성은 "잘 모으는 것"**이야. 이 둘은 동전의 양면이지. 관련 없는 것들이 한곳에 뭉쳐 있으면 모듈성이 깨지고, 관련 있는 것들이 여기저기 흩어져 있으면 응집성이 깨져. 이걸 가장 직관적으로 보여주는 게 "God 클래스"야 — 데이터 접근, 비즈니스 로직, 유효성 검증, 포매팅까지 다 하는 3000줄짜리. "주문 관련 코드를 수정해야 하는데 어디를 봐야 하지?" "이 클래스 전체를 읽어봐." 이런 상황이 응집성 바닥인 코드의 전형이야.

응집성을 높이는 가장 효과적인 도구는 DDD의 바운디드 컨텍스트야. "고객"이라는 개념이 결제 컨텍스트에서는 "결제 수단을 가진 주체"이고 배송 컨텍스트에서는 "배송 주소를 가진 주체"잖아. 이걸 하나의 Customer 클래스로 만들면 응집성이 떨어져. 컨텍스트마다 별도의 모델을 두면 각 모델이 하나의 이유로만 변경되는 깔끔한 구조가 나와.

여기서 중요한 인사이트가 하나 있어. 고응집이면 저결합이 자연스럽게 따라와. 관련 있는 것들을 한 모듈에 잘 모아두면 다른 모듈과의 연결 지점이 줄어들거든. "이 기능을 어느 모듈에 넣을까?" 고민할 때, 관련 기능이 이미 있는 모듈에 넣으면 결합도도 같이 낮아지는 거야. 이게 **"고응집, 저결합"**이라는 소프트웨어 설계의 황금률이야.

응집성이 부족하면 대가가 커. 하나의 기능을 수정하려고 여러 모듈을 동시에 건드려야 하고, 새 팀원 온보딩에 시간이 오래 걸리고, 예상치 못한 테스트가 깨지고, "여기도 고쳐야 하는 줄 몰랐어"가 반복돼. 그리고 이건 기술 계층(controller, service, repository)으로 나눌 때 특히 심해져. 도메인(order, payment, shipping)으로 나눠야 주문 관련 변경이 생겼을 때 한 패키지 안에서만 작업할 수 있어.

그렇다면 코드 품질에서 가장 중요한 속성 하나를 고르라면? **관심사 분리(Separation of Concerns)**야. 이게 결국 한 가지 질문으로 귀결돼 — "비즈니스 로직과 기술 인프라를 섞고 있지 않은가?"

프레드 브룩스가 본질적 복잡성우발적 복잡성을 구분했잖아. 은행 시스템이 복잡한 건 도메인 자체가 복잡하기 때문이고(본질적), DB 연결 관리나 JSON 직렬화가 복잡한 건 우리가 선택한 기술 때문이야(우발적). 이 두 종류의 복잡성이 코드에서 뒤섞이면 양쪽 모두 이해하기 어렵고 변경하기 어려워져. 비즈니스 로직을 테스트하는데 웹 서버를 띄우고 DB를 연결하고 외부 API를 호출해야 한다면 — 그건 관심사가 분리되지 않았다는 명확한 신호야.

이걸 아키텍처 수준에서 해결하는 게 **육각형 아키텍처(포트와 어댑터)**야. 핵심 아이디어는 간단해. 비즈니스 로직을 중앙에 두고, 외부 세계와의 통신은 포트(인터페이스)와 어댑터(구현체)를 통해서만 해. "나는 이런 방식으로 데이터를 저장하고 싶어"라는 포트를 도메인이 정의하면, "PostgreSQL로 저장할게" 또는 "인메모리로 저장할게"라는 어댑터가 실제 구현을 담당하는 거지. 도메인 로직은 어떤 DB를 쓰는지, 어떤 웹 프레임워크를 쓰는지 전혀 몰라.

이 분리를 달성하는 데는 두 방향이 있어. 하향식으로는 바운디드 컨텍스트로 시스템 전체를 조감하면서 "여기서부터 여기까지가 하나의 관심사"라고 큰 경계를 긋고, 상향식으로는 TDD로 코드를 작성하면서 "이 부분은 따로 테스트해야겠다"는 감각을 통해 세부 관심사를 분리해. 이 두 접근이 만나는 지점에서 가장 좋은 설계가 나와.

한 가지 더 — API를 설계할 때 "함수 호출을 네트워크로 옮기면 되는 거 아니야?"라고 생각하면 안 돼. 함수 호출은 동기적이고 빠르고 실패하지 않는데, API 호출은 비동기적이고 느리고 언제든 실패할 수 있거든. 재시도, 서킷 브레이커, 타임아웃 같은 분산 시스템의 관심사와 비즈니스 로직은 완전히 별개야. 이것도 관심사 분리야.


정리

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

  1. 모듈성은 변경의 영향 범위를 제한하는 것이고, 응집성은 관련 있는 것을 한곳에 모으는 것이야. 이 둘은 동전의 양면이고, "고응집이면 저결합이 따라온다"가 설계의 황금률이야.
  2. 본질적 복잡성과 우발적 복잡성을 섞지 마. 육각형 아키텍처(포트와 어댑터)가 비즈니스 로직과 기술 인프라를 구조적으로 분리해줘.
  3. TDD가 모듈성과 관심사 분리의 리트머스 테스트야. 테스트가 어려우면 설계가 나쁜 거고, 하향식(바운디드 컨텍스트)과 상향식(TDD)을 함께 쓰면 최적의 설계가 나와.