Architecture & Design/Software Architecture
Principles - 04. 의존성 규칙
부장이 말단 사원에게 의존하면 조직이 뒤집힌다 — 의존성 규칙
조직도를 그린다고 하자. 사장이 부장에게 지시하고, 부장이 대리에게, 대리가 사원에게. 화살표는 위에서 아래로 향한다. 만약 사원이 "제가 이 방식으로 하겠습니다"라고 바꾸면 사장의 업무가 영향을 받는다면, 조직이 뒤집힌 것이다 — 말단이 최상위를 좌지우지하면 안 된다.
소프트웨어도 같다. 도메인(최상위)이 데이터베이스(최하위)에 의존하면, DB를 바꿀 때 도메인이 흔들린다. 의존성 규칙은 "의존성은 항상 안쪽(고수준)으로만 향해야 한다"고 말한다 — 인프라가 도메인에 맞추지, 도메인이 인프라에 맞추지 않는다.
의존성 역전 — 화살표를 돌려세운다
도메인이 DB를 직접 쓰면, 의존성이 도메인 → DB로 향한다. DB를 바꾸면 도메인이 깨진다. 의존성 역전(Dependency Inversion)은 이 화살표를 돌려세운다 — 도메인이 인터페이스를 정의하고, DB 구현체가 그 인터페이스를 구현한다. 그러면 의존성이 DB → 도메인(인터페이스)으로 향한다.
flowchart TB
subgraph Without["의존성 역전 없음"]
Domain1["OrderService<br/>(도메인)"] --> DB1["JpaOrderRepository<br/>(인프라)"]
Note1["DB를 바꾸면 도메인이 깨짐"]
end
subgraph With["의존성 역전 적용"]
Domain2["OrderService<br/>(도메인)"] --> Iface["OrderRepository<br/>(인터페이스, 도메인에 정의)"]
DB2["JpaOrderRepository<br/>(인프라)"] --> Iface
Note2["DB를 바꿔도 도메인은 모름"]
end
안정된 의존성 원칙 (SDP)과 안정된 추상성 원칙 (SAP)
Robert Martin은 의존성 방향을 정하는 두 보조 원칙을 세웠다. 안정된 의존성 원칙(SDP)은 "덜 안정적인 모듈이 더 안정적인 모듈에 의존해야 한다"다 — 자주 바뀌는 모듈이 안정적인 모듈에 기대는 것이지, 그 반대가 아니어야 한다. 안정된 추상성 원칙(SAP)은 "안정적인 모듈은 추상적(인터페이스)이어야 한다"다 — 안정적인데 구체적이면(구현 클래스), 의존하는 쪽이 바꾸지 못해 갇힌다.
설계 사례 — 의존성 역전을 적용한 주문 서비스
// 의존성 역전 없음 — 도메인이 인프라에 직접 의존
class OrderService {
private final JpaOrderRepository repo = new JpaOrderRepository(); // 인프라 고정
void place(Order order) {
repo.save(order); // JPA에 묶임
}
}
// PostgreSQL → MongoDB로 바꾸면 OrderService 코드를 열어야 함
// 의존성 역전 적용 — 도메인이 인터페이스 정의, 인프라가 구현
interface OrderRepository { // 도메인 계층에 정의됨
void save(Order order);
}
class OrderService {
private final OrderRepository repo; // 인터페이스에 의존
OrderService(OrderRepository repo) { this.repo = repo; }
void place(Order order) { repo.save(order); }
}
// 인프라 계층 (도메인을 모르는 곳에서 구현)
class MongoOrderRepository implements OrderRepository {
void save(Order order) { mongoCollection.insert(order); }
}
// DB 교체: MongoOrderRepository → RedisOrderRepository로 주입만 교체
의존성 역전의 핵심 — 인터페이스를 도메인 계층에 둔다는 것이다. 인터페이스가 인프라 계층에 있으면, 도메인이 인프라 패키지에 의존하게 되어 여전히 묶인다. 인터페이스를 도메인이 소유하면, 인프라가 도메인을 따르게 된다 — 조직도로 돌아가면, 사원이 규칙을 만드는 게 아니라 사장이 규칙을 만들고 사원이 따르는 것과 같다.
의존성 규칙이 적용되는 층
클린 아키텍처에서 의존성 규칙은 모든 층에 적용된다. 각 층은 자기보다 안쪽(고수준) 층에만 의존하고, 바깥쪽(저수준)을 모른다.
flowchart TB
subgraph Outer["외부 (저수준)"]
Web["웹 컨트롤러<br/>(Spring MVC)"]
DB["DB 어댑터<br/>(JPA)"]
MQ["메시지 어댑터<br/>(Kafka)"]
end
subgraph Mid["중간 (유스케이스)"]
UC["OrderUseCase<br/>(응용 서비스)"]
end
subgraph Inner["내부 (도메인, 최고수준)"]
Domain["Order 엔터티<br/>OrderRepository 인터페이스"]
end
Web --> UC
UC --> Domain
DB -. 구현 .-> Domain
MQ -. 구현 .-> Domain
화살표가 모두 안쪽으로 향한다. 웹 컨트롤러가 유스케이스를 알고, 유스케이스가 도메인을 안다. 하지만 도메인은 유스케이스를 모르고, 유스케이스는 웹을 모른다. DB 어댑터와 메시지 어댑터는 도메인이 정의한 인터페이스를 구현한다.
// 의존성 규칙 위반 — 도메인이 외부를 앎
class Order {
@Entity // JPA 애노테이션 (인프라)
@Table(name = "orders")
private List<OrderLine> lines;
void confirm() {
kafkaTemplate.send("order-events", this); // Kafka 직접 호출 (인프라)
}
}
// JPA를 MyBatis로 바꾸면 @Entity를 떼야 함 → 도메인 코드 수정
// Kafka를 RabbitMQ로 바꾸면 kafkaTemplate 호출 수정 → 도메인 코드 수정
// 의존성 규칙 준수 — 도메인이 외부를 모름
class Order {
private final List<OrderLine> lines; // 순수 자바, 애노테이션 없음
private OrderStatus status;
OrderPlaced confirm() {
if (lines.isEmpty()) throw new IllegalStateException();
status = OrderStatus.PLACED;
return new OrderPlaced(id, total()); // 이벤트 반환 — 어디로 가는지 모름
}
// JPA도, Kafka도 모름 — 도메인은 순수 자바 객체
}
의존성 규칙을 지키면 도메인은 순수 자바 객체다 — 프레임워크 애노테이션 없이, 외부 기술 import 없이, 오직 도메인 규칙만으로 구성된다. 이 순수성이 헥사고날 아키텍처의 뼈대이고, 테스트 용이성의 근원이다.
의존성 역전 위반을 코드에서 감지하는 법
코드 리뷰에서 의존성 규칙 위반을 발견하는 신호들이다. 하나라도 보이면 의존성 방향을 의심한다.
| 신호 | 의미 | 수정 방향 |
|---|---|---|
도메인 클래스에 @Entity, @Table |
도메인이 JPA에 의존 | 매핑을 별도 어댑터로 분리 |
도메인이 RestTemplate, KafkaTemplate 직접 import |
도메인이 외부 기술을 앎 | 인터페이스(포트) 뒤로 숨김 |
도메인이 import org.springframework.* |
프레임워크에 묶임 | Spring 애노테이션을 어댑터 계층으로 이동 |
서비스가 구체 Repository(JpaXxxRepository)를 직접 사용 |
인프라 구현에 의존 | 도메인 정의 인터페이스에 의존하도록 변경 |
안정성으로 의존 방향을 판단한다
Martin의 안정된 의존성 원칙(SDP)은 "변하기 쉬운 모듈이 안정적인 모듈에 의존해야 한다"다. 안정성은 "다른 모듈이 나에게 얼마나 의존하는가"(들어오는 화살표 수)로 잰다 — 많은 모듈이 의존할수록 안정적이다(바꾸기 어렵다). 안정된 추상성 원칙(SAP)은 "안정적인 모듈은 추상적(인터페이스)이어야 한다"다 — 안정적인데 구체적이면, 의존하는 쪽이 바꾸지 못해 갇힌다.
// SDP 위반 — 안정적인 도메인이 불안정한 유틸리티에 의존
class Order { // 많은 모듈이 Order에 의존 (안정적)
private final DateUtils utils = new DateUtils(); // DateUtils는 자주 바뀜 (불안정)
// → 안정적인 모듈이 불안정한 모듈에 의존 → SDP 위반
}
// SDP 준수 — 불안정한 쪽이 안정적인 추상에 의존
class DateUtils implements ClockProvider { // 불안정한 구현이 안정적 추상(인터페이스)을 구현
Instant now() { return clock.instant(); }
}
class Order {
private final ClockProvider clock; // 안정적 추상에 의존 — 구현체 바뀌어도 안 흔들림
}
안정성은 "자주 바뀌는가"로도 판단한다. 도메인 규칙(주문 취소 조건)은 자주 안 바뀌므로 안정적이고, 인프라(DB 종류, 프레임워크 버전)는 자주 바뀌므로 불안정하다. 불안정한 것(인프라)이 안정적인 것(도메인)에 의존하게 해야지, 그 반대가 되면 안 된다 — 조직도로 돌아가면, 자주 바뀌는 말단 사원이 자주 안 바뀌는 사장 규칙에 맞춰야지, 사장이 사원에게 맞추면 안 되는 것과 같다.
참고
- Martin, Robert C. —
(Pearson, 2017), Ch.13-15 (의존성 규칙, SDP, SAP) — 접근 2026-07-15 - Martin, Robert C. —
(Pearson, 2002) (DIP, SDP, SAP 원전) — 접근 2026-07-15
'Architecture & Design > Software Architecture' 카테고리의 다른 글
| Principles - 06. 계약 설계와 API (0) | 2026.07.16 |
|---|---|
| Principles - 05. 캡슐화와 정보 은닉 (0) | 2026.07.16 |
| Principles - 03. 패러다임의 함의 (0) | 2026.07.16 |
| Principles - 02. 추상화와 깊은 모듈 (0) | 2026.07.16 |
| Principles - 01. SOLID 원칙 (0) | 2026.07.15 |