Clean Architecture 34 ‐ 빠져 있는 장 - thought-corner/backend-roadmap GitHub Wiki

계층 기반 패키지

  • 가장 단순한 첫 번째 설계 방식은 전통적인 수평 계층형 아키텍처이다.
  • 기술적인 관점에서 해당 코드가 하는 일에 기반해 그 코드를 분할한다.
  • 이 전형적인 계층형 아키텍처에는 웹, '업무 규칙', 영속성 코드를 위해 계층이 각각 하나씩 존재한다. 다시 말해 코드는 계층이라는 얆은 수평 조각으로 나뉘며, 각 계층은 유사한 종류의 것들을 묶는 도구로 사용된다. '엄격한 계층형 아키텍처'의 경우 계층은 반드시 바로 아래 계층에만 의존해야 한다.
    • OrdersController : 웹 컨트롤러이며, 웹 기반 요청을 처리한다. Spring MVC 컨트롤러 등이 여기에 해당한다.
    • OrdersService : 주문 관련 '업무 규칙'을 정의하는 인터페이스
    • OrdersServiceImpl : OrdersService의 구현체
    • OrdersRepository : 영구 저장된 주문 정보에 접근하는 방법을 정의하는 인터페이스
    • JdbcOrdersRepository : OrdersRepository 인터페이스의 구현체
  • 계층형 아키텍처는 업무 도메인에 대해 아무것도 말해주지 않는다는 문제가 있다.

기능 기반 패키지

  • 코드를 조직화하는 두 번째 방식으로, 계층이 아닌 기능(feature) 단위로 코드를 분할한다.
  • 계층 패키지를 모두 없애고, 주문과 관련된 모든 코드를 order 패키지 하나에 밀어 넣는다.
    • SendOrderService : '주문 보내기' 유스케이스를 구현한다. 클래스 이름에 유스케이스가 드러나므로 코드에서 무슨 일을 하는지 바로 알 수 있다.
    • Order : 주문 도메인 모델. 유효성 검증과 비즈니스 규칙을 스스로 책임진다.
    • SendOrderController : 웹 요청을 받아 유스케이스로 전달한다.
    • OrderRepository : 주문 영속성 접근을 정의하는 인터페이스
    • JdbcOrderRepository : OrderRepository 의 구현체
  • 가장 큰 장점은 패키지 경계를 실제 접근 제어자로 강제할 수 있다는 점이다. 같은 패키지 안에 있으므로 외부에 노출할 필요가 없는 클래스는 package-private 으로 선언해 다른 기능에서 접근하지 못하게 막는다.
  • 패키지 이름이 곧 도메인을 드러내기 때문에 이른바 '소리치는 아키텍처(screaming architecture)'에 가까워진다. 계층형과 달리 코드 구조만 봐도 이 애플리케이션이 무엇을 하는지 알 수 있다.
  • 다만 계층형보다 아키텍처의 가시성은 오히려 더 떨어진다는 문제가 있다.
  • 어댑터를 나타내는 패키지가 없고, 인커밍/아웃고잉 포트를 구분하는 장치도 없다.
  • package-private 접근 제어자는 응집도를 높여주지만, 의존성 방향이 잘못되는 것을 막아주지는 못한다. 여전히 도메인 코드가 영속성 코드에 의존할 수 있다.

포트와 어댑터

  • 포트와 어댑터 혹은 육각형 아키텍처 경계, 컨트롤러, 엔티티 등의 방식으로 접근하는 이유는 업무/도메인에 초점을 둔 코드가 프레임워크나 데이터베이스 같은 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서다.
  • '내부' 영역은 도메인 개념을 모두 포함하는 반면, '외부' 영역은 외부 세계와의 상호작용을 포함한다.
  • 여기서 주요 규칙은 바로 '외부'가 '내부'에 의존하며, 절대 그 반대로는 안 된다.

컴포넌트 기반 패키지

  • Simon Brown이 제안한 방식으로, 계층 기반과 기능 기반의 절충안에 가깝다. 계층형처럼 웹 계층은 분리하되, '업무 로직 + 영속성'을 하나의 컴포넌트로 묶어 봉인한다.
  • 즉, OrdersServiceOrdersRepository를 서로 다른 계층 패키지에 두지 않고, orders라는 하나의 컴포넌트 패키지 안에 함께 넣는다.
    • OrdersController : 웹 계층. 별도 패키지에 존재하며 컴포넌트의 공개 인터페이스만 호출한다.
    • OrdersComponent(혹은 OrdersService) : 컴포넌트의 유일한 공개 진입점. 외부에서 주문 기능에 접근하는 통로다.
    • OrdersComponentImpl : package-private. 외부에서 직접 참조할 수 없다.
    • OrdersRepository, JdbcOrdersRepository : 모두 package-private. 영속성 접근은 컴포넌트 내부의 구현 세부사항이 된다.
  • 핵심은 컴포넌트 외부에서 리포지토리를 직접 호출하는 일이 컴파일 자체가 되지 않는다는 점이다. 계층형에서 흔한 "컨트롤러가 리포지토리를 바로 찌르는" 우회 경로가 원천 차단된다.
  • 컴포넌트가 곧 배포 가능한 경계 후보이므로, 나중에 마이크로서비스로 떼어내기 좋은 구조가 된다.
    • 여전히 웹과 나머지를 나누는 수평 계층의 흔적이 남아 있고, 도메인이 영속성 코드와 같은 패키지에 있어 의존성 방향이 섞이기 쉽다.
    • 인커밍/아웃고잉 포트와 어댑터의 구분이 없다. DB를 교체하는 일이 컴포넌트 내부 수술이 된다.
    • DI 프레임워크가 리플렉션으로 package-private 클래스를 다루는 경우, 접근 제어자에 의한 보호가 사실상 무력해질 수 있다.

조직화 vs 캡슐화

  • 만약 자바 애플리케이션에서 모든 타입을 public으로 지정한다면, 패키지는 단순히 조직화를 위한 메커니즘으로 전락해 캡슐화를 위한 메커니즘이 될 수 없다.
  • public 타입을 코드 베이스 어디에서도 사용할 수 있다면 패키지를 사용하는 데 따른 이점이 거의 없다. 따라서 사실상 패키지를 사용하지 않는 것과 같다.
  • 패키지를 무시해 버리면 최종적으로 어떤 아키텍처 스타일로 만들려고 하는지는 아무런 의미가 없어진다.
  • public 지시자를 과용하면 이 장의 앞에서 제시한 네 가지 아키텍처 접근법은 본질적으로 완전히 같아진다.