클린 아키텍처 학습 메모: 정책 수준과 의존성 방향

반응형

클린 아키텍처에서 고수준 정책은 HTTP, 데이터베이스, 이메일 같은 입출력 세부사항보다 업무 규칙에 가깝다. 핵심은 실행 중 데이터가 어느 방향으로 흐르느냐가 아니라, 소스 코드 의존성이 변경 가능성이 큰 세부사항에서 중요한 정책 쪽을 향하도록 경계를 만드는 것이다.

『클린 아키텍처』 19장 ‘정책과 수준’을 읽으며 적어 둔 메모를 그대로 옮기기보다, 주문 취소 예제로 다시 풀어 봤다.

수준은 기술 이름이 아니라 정책의 위치다

주문 시스템을 다음 세 부분으로 나눠 보자.

  • 주문이 취소 가능한지 판단하고 환불액을 계산하는 규칙
  • HTTP 요청을 읽고 응답을 만드는 controller
  • JPA로 주문을 조회하고 외부 결제 API를 호출하는 adapter

첫 번째는 입력 장치와 출력 기술이 바뀌어도 유지되어야 할 고수준 정책이다. 나머지는 정책을 실행하기 위해 바깥 세계와 연결하는 세부사항이다. Controller → Service → Repository라는 클래스 이름만으로 수준이 정해지는 것은 아니다. business rule이 persistence annotation과 HTTP DTO에 묶여 있다면 이름과 달리 경계가 뒤섞여 있다.

고수준 정책은 보통 기술 세부사항보다 덜 자주, 더 중요한 이유로 변경되는 경향이 있다. 그러나 ‘안쪽 코드는 절대 변하지 않는다’는 법칙은 아니다. 업무 규칙도 자주 바뀔 수 있다. 변경 빈도 하나가 아니라 변경 이유, 업무상 중요도, 독립적으로 테스트할 가치까지 같이 본다.

실행 흐름과 소스 의존성은 다를 수 있다

주문 취소 use case가 데이터베이스에서 주문을 읽어야 한다고 하자. 실행 흐름은 안쪽 정책에서 바깥 adapter를 호출하는 모양이지만, interface를 정책 쪽에 두면 소스 의존성은 반대로 만들 수 있다.

// use-case/core 쪽의 계약
interface OrderRepository {
    Order findById(long orderId);
}

final class CancelOrder {
    private final OrderRepository orders;

    CancelOrder(OrderRepository orders) {
        this.orders = orders;
    }

    CancellationResult execute(long orderId) {
        Order order = orders.findById(orderId);
        return order.cancel();
    }
}

// 바깥 infrastructure adapter
final class JpaOrderRepository implements OrderRepository {
    @Override
    public Order findById(long orderId) {
        // JPA 조회와 domain 변환
        throw new UnsupportedOperationException("example");
    }
}

런타임 호출은 CancelOrder → JpaOrderRepository로 일어나지만, core는 JpaOrderRepository를 모른다. 바깥 adapter가 core의 OrderRepository 계약을 알고 구현한다. 의존성 역전으로 데이터베이스 교체와 core unit test의 영향 범위를 줄이는 방식이다.

소스 의존성을 Java의 import 한 줄로만 정의하면 부족하다. 타입 이름, 상속·구현 관계, annotation, build module dependency처럼 한쪽 소스를 고치거나 컴파일할 때 다른 쪽을 알아야 하는 관계를 함께 봐야 한다.

같은 이유로 바뀌는 정책을 묶는다

경계는 단순히 파일 수를 늘리는 도구가 아니다. 동일한 이유와 시점에 바뀌는 정책은 함께 두고, 다른 이유로 바뀌는 정책은 분리한다.

예를 들어 환불 규정과 주문 취소 가능 시간은 같은 상품 정책 변경으로 함께 바뀔 수 있다. 반면 JPA mapping은 schema나 persistence 선택 때문에 바뀌고, HTTP controller는 API 계약 때문에 바뀐다. 이 변경 이유들이 한 component에 섞이면 작은 기술 변경이 핵심 정책까지 흔든다.

그렇다고 모든 class 앞에 interface를 만들 필요는 없다. 교체 가능성도 없고 분리된 테스트 가치도 작은 code까지 경계로 감싸면 탐색 비용만 늘어난다. 다음 질문에 답할 수 있는 지점에 경계를 둔다.

  • 서로 다른 이유로 변경되는가?
  • 한쪽 세부사항을 바꿀 때 핵심 규칙도 다시 배포하거나 테스트해야 하는가?
  • 정책을 기술 없이 독립적으로 검증할 가치가 있는가?
  • 여러 adapter가 같은 정책 계약을 사용할 가능성이 있는가?

component 의존성은 순환하지 않게 만든다

component 관계는 방향성이 있는 비순환 그래프가 되어야 독립적인 변경과 배포 순서를 판단하기 쉽다. A → B → C → A 순환이 생기면 어느 component도 다른 것 없이 빌드하거나 이해하기 어렵다.

순환을 없애려고 모든 공통 code를 거대한 common module로 옮기면 문제를 숨길 수 있다. 어느 정책이 계약을 소유해야 하는지 먼저 정하고, 필요하면 의존성 역전이나 별도 안정된 abstraction으로 방향을 끊는다. 공통 module도 명확한 변경 이유가 있을 때만 component가 된다.

남은 판단 기준

정책 수준은 폴더 계층을 깊게 만드는 문제가 아니다. 중요한 업무 규칙이 프레임워크와 입출력 세부사항을 직접 알아야 하는지 묻는 기준이다. 데이터 흐름은 바깥으로 나가더라도 source dependency는 안쪽 정책을 향할 수 있다. 이 방향을 지키되, 실제 변경 이유가 없는 곳까지 형식적인 interface를 늘리지 않는 균형이 필요하다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글