학습 목표
결합도(coupling) 와 응집도(cohesion) 의 의미·트레이드오프를 설명할 수 있다.
느슨한 결합·높은 응집이 변경 영향 범위를 줄이는 이유를 설명할 수 있다.
패키지 by layer vs by feature 구조의 장단점을 비교할 수 있다.
모듈 경계·공개 API·의존 방향으로 결합을 측정·개선하는 방법을 설명할 수 있다.
문제 상황
util 패키지에 300개 클래스 — 이름만 다른 헬퍼
order 기능 수정인데 payment·notification·report까지 동시 PR
OrderService가 PaymentService 내부 DTO를 직접 import
common 모듈이 모든 도메인 entity를 알고 있다
- 테스트 하나 돌리려면 전체 Spring context 기동
SOLID는 클래스·인터페이스 단위 규율이다. 이번 편은 모듈·패키지가 서로 얼마나 묶이고, 안에서 얼마나 한 목적을 갖는지다.
1. 결합도와 응집도
| |
결합 (Coupling) |
응집 (Cohesion) |
| 질문 |
모듈끼리 얼마나 의존하는가 |
모듈 안 요소가 한 목적에 모였는가 |
| 목표 |
낮을수록 좋음 (느슨한 결합) |
높을수록 좋음 |
| 나쁜 신호 |
import 지옥·순환·공유 mutable |
God package·"misc"·날짜만 같은 파일 |
- 결합도 — 한 모듈 변경이 다른 모듈에 미치는 정도
- 응집도 — 한 모듈 안 코드가 같은 이유로 함께 바뀌는 정도
- 목표 조합: loose coupling + high cohesion (Richards/Ford)
결합도 유형 (실무에서 자주 보는 것)
| 유형 |
설명 |
예 |
| Data |
공개 API·DTO로만 데이터 전달 |
PlaceOrderCommand |
| Stamp |
불필요하게 큰 구조체 전달 |
Entity 전체 넘김 |
| Control |
다른 모듈 흐름을 직접 제어 |
if (payment.isOk()) order.next() |
| Common |
공유 DB·상수·global state |
common entity 전역 |
| Content |
내부 구현 직접 접근 |
private 필드·SQL 직접 |
- Data coupling이 이상적에 가깝다 — 나머지는 리팩터링 후보
- Content·Common은 아키텍처 경계 붕괴 신호
응집도 유형
| 유형 |
설명 |
| Functional |
모듈 하나 = 기능 하나 (이상) |
| Sequential |
출력 → 다음 단계 입력 파이프라인 |
| Communicational |
같은 데이터를 다루는 연산 |
| Logical |
"logging"처럼 카테고리만 같음 (약함) |
| Coincidental |
우연히 한 폴더 (최악) |
misc·helper·util 과다 = coincidental/logical 응집 의심
2. SOLID와의 관계
| SOLID |
결합·응집 |
| SRP |
한 모듈 한 변경 이유 → functional cohesion |
| ISP |
쓰지 않는 API 의존 제거 → coupling ↓ |
| DIP |
추상화 경유 → 구현 coupling ↓ |
- SOLID 위반이 누적되면 모듈 간 결합도가 올라간다
- 반대로 패키지 경계 없이 SOLID만 클래스에 적용하면 가짜 분리
3. 패키지 구조
By layer (기술 계층)
com.app.controller
com.app.service
com.app.repository
com.app.dto
| 장점 |
단점 |
| 익숙, 프레임워크 예제와 동일 |
기능 하나가 여러 패키지에 흩어짐 |
| 계층 규칙 lint 쉬움 |
기능 추가·삭제 시 가로 변경 |
By feature (기능·capability)
com.app.order
com.app.payment
com.app.catalog
├── domain
├── application
└── adapter
| 장점 |
단점 |
| 변경이 한 vertical slice 안에 |
패키지 간 중복·공유 규칙 필요 |
| 팀 ownership·MSA 분해와 정렬 |
계층 규칙을 모듈마다 지켜야 함 |
- 모듈러 모놀리스 — Gradle multi-module + by feature가 흔한 조합
code/package-layout.md — 두 방식 예시
공개 API와 캡슐화
- 모듈 바깥에 노출하는 것: port·DTO·facade만
internal 패키지(Java) · @VisibleForTesting — 의도적 경계
- ArchUnit · module-info —
order → payment 단방향 검증
4. 변경 전파
| 구조 |
Payment API 변경 시 |
| 강결합 |
Order·Notification·Report 전부 수정 |
| 경계 + port |
PaymentGateway 어댑터만, Order는 interface 유지 |
강결합: A ── B ── C ── D (한 곳 수정 → 연쇄)
경계: A → [port] ← B_impl C는 A API만 사용
| 측정·개선 |
방법 |
| Fan-out |
모듈이 import하는 외부 모듈 수 |
| Fan-in |
모듈에 의존하는 수 — 높으면 안정 core |
| 순환 |
dependency-cruiser, ArchUnit noCycles |
| 공유 DB |
스키마 coupling — bounded context로 분리 |
- Ripple effect 줄이기: 작은 공개 surface, 이벤트로 async decouple (필요 시)
5. 실무 체크리스트
| 질문 |
좋은 답 |
| 이 PR이 몇 모듈을 건드리는가 |
1~2 capability |
| 다른 팀 모듈 내부를 import했는가 |
port/DTO만 |
common에 도메인 logic이 있는가 |
없음 — infra·진짜 공통만 |
| 테스트가 한 모듈만 로드하는가 |
가능 |
| 삭제할 기능 폴더 하나로 지울 수 있는가 |
by feature면 Yes |
흔한 안티패턴
| 이름 |
증상 |
| Big ball of mud |
경계 없음, 어디든 import |
| Shared kernel abuse |
common에 모든 entity |
| Layer lasagna |
layer만 있고 기능 단위 ownership 없음 |
| Shotgun surgery |
한 요구사항에 파일 30개 |
6. 정리
- 느슨한 결합 — 변경·테스트·배포 blast radius 축소
- 높은 응집 — 한 모듈 = 한 capability·한 actor
- by feature + 모듈 내부 hexagonal — 실무에서 자주 쓰는 조합
- 공개 API 최소화, data coupling, port 경유가 경계를 만든다
- ArchUnit·모듈 그래프로 순환·fan-out을 주기적으로 본다
다음에 다룰 것
- 도메인 주도 설계 입문
- Bounded Context, Ubiquitous Language, Context Map
해당 내용은 Fundamentals of Software Architecture (Mark Richards, Neal Ford) 및 Clean Architecture (Robert C. Martin) 의 내용을 기반으로 합니다.