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이 제안한 방식으로, 계층 기반과 기능 기반의 절충안에 가깝다. 계층형처럼 웹 계층은 분리하되, '업무 로직 + 영속성'을 하나의 컴포넌트로 묶어 봉인한다.
즉, OrdersService와 OrdersRepository를 서로 다른 계층 패키지에 두지 않고, orders라는 하나의 컴포넌트 패키지 안에 함께 넣는다.
OrdersController : 웹 계층. 별도 패키지에 존재하며 컴포넌트의 공개 인터페이스만 호출한다.
OrdersComponent(혹은 OrdersService) : 컴포넌트의 유일한 공개 진입점. 외부에서 주문 기능에 접근하는 통로다.
OrdersComponentImpl : package-private. 외부에서 직접 참조할 수 없다.
OrdersRepository, JdbcOrdersRepository : 모두 package-private. 영속성 접근은 컴포넌트 내부의 구현 세부사항이 된다.
핵심은 컴포넌트 외부에서 리포지토리를 직접 호출하는 일이 컴파일 자체가 되지 않는다는 점이다. 계층형에서 흔한 "컨트롤러가 리포지토리를 바로 찌르는" 우회 경로가 원천 차단된다.
컴포넌트가 곧 배포 가능한 경계 후보이므로, 나중에 마이크로서비스로 떼어내기 좋은 구조가 된다.
여전히 웹과 나머지를 나누는 수평 계층의 흔적이 남아 있고, 도메인이 영속성 코드와 같은 패키지에 있어 의존성 방향이 섞이기 쉽다.
인커밍/아웃고잉 포트와 어댑터의 구분이 없다. DB를 교체하는 일이 컴포넌트 내부 수술이 된다.
DI 프레임워크가 리플렉션으로 package-private 클래스를 다루는 경우, 접근 제어자에 의한 보호가 사실상 무력해질 수 있다.
조직화 vs 캡슐화
만약 자바 애플리케이션에서 모든 타입을 public으로 지정한다면, 패키지는 단순히 조직화를 위한 메커니즘으로 전락해 캡슐화를 위한 메커니즘이 될 수 없다.
public 타입을 코드 베이스 어디에서도 사용할 수 있다면 패키지를 사용하는 데 따른 이점이 거의 없다. 따라서 사실상 패키지를 사용하지 않는 것과 같다.
패키지를 무시해 버리면 최종적으로 어떤 아키텍처 스타일로 만들려고 하는지는 아무런 의미가 없어진다.
public 지시자를 과용하면 이 장의 앞에서 제시한 네 가지 아키텍처 접근법은 본질적으로 완전히 같아진다.
🗂️ Page Index for this GitHub Wiki