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