이전에 의존성 역전하기에 대해 작성하였습니다. 이번에는 만들면서 배우는 클린 아키텍처의 3장 내용을 읽고, 코드를 계층으로 구성하는 방법과 기능으로 구성하는 방법, 그리고 아키텍처가 드러나도록 패키지를 구성하는 방법에 대해 정리하도록 하겠습니다.
계층으로 구성하기
코드를 구조화하는 첫 번째 접근법은 계층을 이용하는 것으로, 아래와 같이 domain, persistence, web 패키지로 코드를 구성할 수 있습니다.
책에서는 적어도 세 가지 이유로 이 패키지 구조가 최적의 구조가 아니라고 이야기하는데, 그중 두 가지를 정리하면 아래와 같습니다.
첫 번째로, 애플리케이션의 기능 조각이나 특성을 구분 짓는 패키지 경계가 없습니다. 사용자를 관리하는 기능을 추가해야 한다면 web 패키지에 UserController를 추가하고, domain 패키지에 UserService, UserRepository, User를 추가하고, persistence 패키지에 UserRepositoryImpl을 추가하게 됩니다. 그림의 빨간 점선 박스가 이렇게 추가되는 클래스입니다. 추가적인 구조가 없다면, 서로 연관되지 않은 기능들끼리 예상하지 못한 부수 효과를 일으킬 수 있는 클래스들의 묶음으로 빠르게 변할 가능성이 큽니다.
두 번째로, 애플리케이션이 어떤 유스케이스들을 제공하는지 파악할 수 없습니다. AccountService와 AccountController라는 이름만으로는 어떤 유스케이스를 구현했는지 알기 어렵습니다.
기능으로 구성하기
다음 접근법은 '계층으로 구성하기' 방법의 몇 가지 문제를 해결하기 위해 예제 코드를 기능으로 구성한 것입니다.
가장 본질적인 변경은 계좌와 관련된 모든 코드를 최상위의 account 패키지에 넣었다는 것입니다.
장점
각 기능을 묶은 새로운 그룹은 account와 같은 레벨의 새로운 패키지로 들어가고, 패키지 외부에서 접근되면 안 되는 클래스들에 대해 package-private 접근 수준을 이용해 경계를 강화할 수 있습니다.
또한 AccountService의 책임을 좁히기 위해 클래스명을 SendMoneyService로 바꿨습니다. 이제 '송금하기' 유스케이스를 구현한 코드는 클래스명만으로 찾을 수 있습니다.
단점
다만 이 방식은 계층에 의한 패키징 방식보다 아키텍처의 가시성을 훨씬 더 떨어뜨립니다. 어댑터를 나타내는 패키지명이 없고, 인커밍 포트와 아웃고잉 포트도 확인할 수 없습니다.
또한 도메인 코드와 영속성 코드 간의 의존성을 역전시켜 SendMoneyService가 AccountRepository 인터페이스만 알고 구현체는 알 수 없도록 했지만, 같은 패키지 안에 있기 때문에 package-private 접근 수준을 이용해 도메인 코드가 실수로 영속성 코드에 의존하는 것을 막을 수는 없습니다. 그림에서 SendMoneyService가 AccountRepositoryImpl을 직접 가리키는 빨간 점선이 이 경우입니다.
아키텍처적으로 표현력 있는 패키지 구조
육각형 아키텍처에서 구조적으로 핵심적인 요소는 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터입니다. 이 요소들이 패키지 이름으로 드러나도록 구성하면 아래와 같습니다.
adapter 패키지에 들어 있는 모든 클래스는 application 패키지 내에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출되지 않기 때문에 package-private 접근 수준으로 둬도 됩니다. 그러므로 애플리케이션 계층에서 어댑터 클래스로 향하는 우발적인 의존성은 생길 수 없습니다.
마무리
오늘은 클린 아키텍처의 패키지 구조에 대해 정리하였습니다. 계층형보다는 아키텍처가 드러나는 패키지 구조를 통해 우발적인 의존성을 제거하고, 좀 더 원활하게 서비스를 찾을 수 있도록 구성해야 한다는 것을 알 수 있었습니다. 더 많은 내용이 있지만 9장과 다음 장에서 코드를 통해 설명한다고 하니, 그 부분에서 좀 더 자세하게 작성하려고 합니다. 다음 글에서는 유스케이스 구현하기에 대해 정리하도록 하겠습니다.
댓글
GitHub 계정으로 로그인하시면 댓글과 질문을 남기실 수 있습니다. 남겨 주신 글은 이 저장소의 Discussions에 쌓입니다.