헥사고날 아키텍처를 적용하여 패키지 구조를 정리해봤다.
시스템 구성과 동작원리 그리고 시스템의 구성환경 등을 설명하는 설계도이다.
'클린 코드'의 저자 로버트 마틴이 제안한, 의존성에서 벗어나 유지보수 및 확장이 용이하도록 설계된 아키텍처이다.
위 그림은 로버트 마틴의 블로그 The Clean Architecture에서 가져온 것이다.
화살표는 의존성 규칙이 향하는 방향으로, 바깥 쪽의 원이 안쪽의 원으로 향하고 있는 것을 확인할 수 있다.
객체 지향 설계에서도 중요한 것이 바로 '의존성'이다. 이 의존성 때문에 한 곳을 수정하면 다른 곳에도 영향이 가는(수정해야 하는) 상황이 들이닥친다.
예를 들어, A 프로그램에서 MySQL을 사용하고 있었는데 Oracle로 DB를 변경하기로 했다. 이때, DB를 변경하는 것이 내부 로직, 특히 도메인에 영향을 주지 않도록 한 것이 클린 아키텍처이다.
내가 이해한 대로 적었는데 이 설명이 맞는지는 모르겠다. '클린 아키텍처' 서적을 구입한 후 읽어보는 걸 추천한다.
인터페이스나 기반 요소(infrastructure)의 변경에 영향을 받지 않는 핵심 코드를 만들고 이를 견고하게 관리하는 것이 목표이다.
즉, 이 구조의 핵심은 비즈니스 로직이 표현 로직이나 데이터 접근 로직에 의존하지 않는 것이다.
키워드: 유연성, 테스트 용이성, 유지보수성
키워드: 구현 복잡성, 초반 개발 시간 증가
그림 출처: DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together
그림이 조금 어려울 수 있는데, 크게 안쪽 원인 내부(도메인)와 바깥쪽 원인 외부(인프라)로 구분된다.
내부 영역: 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계
외부 영역: 내부 영역에서 기술을 분리하여 구성한 영역이고 내부 영역 설계 이후 설계
핵심 비즈니스 로직은 중앙의 도메인 영역에 위치하며, 입력과 출력을 처리하는 포트와 어댑터를 통해 외부와 소통한다.
포트는 내부 비즈니스 영역을 외부 영역에 노출한 API(인터페이스)이다. 인바운드(Inbound)/아웃바운드(Outbound) 포트로 구분한다.
어댑터는 외부 세계와 포트 간 교환을 조정하고, 인바운드(Inbound)/아웃바운드(Outbound) 포트로 구분한다.
외부에서 요청해야 동작하는 포트와 어댑터를 주요소(primary)라고 하며, 포트와 어댑터에 따라 주포트 혹은 주어댑터라고 부른다.
애플리케이션이 호출하면 동작하는 포트와 어댑터를 부요소(seconddary)라고 하며, 부포트 또는 부어댑터라고 부른다.
아키텍처를 따르다 보면 자연스럽게 한 형태를 갖추게 된다.
나는 패키지 구조를 만들 때 interface 패키지를 따로 만들지 않고, infrastructure 패키지에 주 어댑터를 함께 넣도록 했다.
주어댑터는 애플리케이션을 일방적으로 알고 있다.
부어댑터는 부포트를 준수해야 한다.
인터페이스 자체를 어느 한 쪽에 치우치게 설계하지 말고, 도메인 관점에서 도메인이 필요로 하는 인터페이스를 설계해야 한다.
아래의 참고 사이트와 GitHub에서 많은 도움을 받았다.
먼저, 찾아보니 다들 Package by Component가 좋은 구조라고 이야기했다. 도메인 별로 패키지를 분리하고, 그 안에서 기능 별로 다시 패키지를 묶어주는 구조라고 이해했다.
실제로 실무에서도 이 구조대로 사용하고 있기도 하다.
그런데, 헥사고날 아키텍처는 각 계층, 특히 도메인 계층을 외부 계층과 분리하는 것이 목표인 구조이다.
즉, 계층 별로 분리하여 한 눈에 볼 수 있어야 한다고 생각했다. 따라서 크게 3개 패키지를 만들어봤다.
애플리케이션 계층은 도메인 계층과 외부 계층 사이에 위치하며, 각 계층의 의존성을 덜어내기 위해 존재한다.
도메인 계층과 외부 계층은 포트라는 인터페이스를 통해 서로 요청을 주고 받는데, 이 포트라는 인터페이스가 속한 계층이 바로 이 애플리케이션 계층이다.
따라서 위와 같이 외부 계층에서 요청이 들어오는 input 포트와 도메인 계층이 요청을 하는 output 포트로 다시 나누어, 그 안에 인터페이스를 배치했다.
찾아봤을 땐 보통 input 포트로 들어오는 걸 usecase라고 불렀다.
제일 중요한 패키지다. 핵심은 외부에 의존하지 않을 것. 특히 프레임워크에 종속되지 않도록 신경써야 한다.
라이브러리는 사용해도 된다고 해서 lombok 라이브러리만 우선 사용 중이다.
도메인 모델이 있는 것은 당연하고, 저 서비스 패키지는 위치를 많이 고민했다.
MemberService
는 input 포트인 ~MemberUseCase
인터페이스의 구현체이다. 내부적으로는 합성(Composition)으로 외부 포트를 사용 중이기도 하다.
즉, 외부에서 들어온 요청을 처리하고 DB 작업이 필요한 경우 외부에 요청하기도 하는 역할을 수행한다.
그래서 '도메인에 영향을 주지 않고, 외부 계층과 내부 계층을 연결시켜주는' 역할이라고 생각해서 애플리케이션 계층에 넣으려고 했었다.
그런데 생각해보니, 여기서 처리하는 로직이 바로 비즈니스 로직이다.
또한 외부로 노출되는 것이 애플리케이션 계층의 포트이고, 그 안의 구현체는 감춰둬야 맞다.
따라서, 도메인 계층에 위치하는 것이 옳다고 생각하고 여기에 뒀다.
제일 고민이 많은 패키지다. 앞서 말했듯, 사용자(클라이언트)에서 요청하는 부분은 interface 패키지로 따로 빼도 괜찮았다.
그런데 참고한 코드들에서도 두 계층을 합쳐뒀었고, input/output으로 같이 보는게 직관적이지 않을까 해서 합쳐보았다.
지금 생각해보면 기능이 많아질 수록 복잡해질 거 같아서, interface로 빼는게 나았을 거란 생각도 든다.
아무튼, 크게 어댑터와 설정 패키지로 나뉜다. config 패키지를 이곳에 둔 까닭은 스프링 빈 등록 등을 처리하는 설정 정보이기 때문에, 외부 종속성에 든다고 판단했기 때문이다.
어댑터 패키지는 다시 input 패키지와 output 패키지로 나뉜다.
input은 외부에서 들어오는 요청을 처리하는 패키지로, 나는 API 서버를 구축할 것이므로 Rest 폴더 아래에 주어댑터를 넣었다.
output은 외부로 나가는 요청을 처리하는 패키지로, 당연하게도 DB와 관련된 클래스과 부어댑터가 있다.
input과 output 안에는 mapper 패키지가 있는데, 여기서 말하는 mapper는 MVC 패턴에서 많이 봤던 DB와 매핑시켜주는 매퍼가 아니다.
외부 계층과 내부 계층(도메인)에서 사용하는 DTO는 각 계층에 맞게 변환시켜 주어야 한다.
즉, 외부 계층의 DTO를 내부 계층에서 사용하는 도메인 모델로 변경하고, 다시 내부 계층에서 사용하는 도메인 모델을 외부 계층의 DTO로 변경하여 각 계층의 의존성을 줄이기 위한 매퍼 클래스이다.
현재 구현체는 따로 만들지 않았는데, 많이 사용한다는 mapstruct 혹은 modelmapper 라이브러리를 사용할 생각이다.
아니면 경험삼아 스스로 만들어봐도 괜찮다고 생각한다.
멘토링을 받으며 패키지 구조를 변경한 사유와 결과를 기입할 것이다.
https://github.com/rbailen/Hexagonal-Architecture
Hexagonal Architecture With Spring Boot | Code With Arho
Package by Component with Clean Modules in Java
Package by Layer vs Package by Feature
Package by Component and Architecturally-aligned Testing
헥사고날(Hexagonal) 아키텍처 in 메쉬코리아 :: MESH KOREA | VROONG 테크 블로그
지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기
헥사고날 아키텍처(Hexagonal Architecture) : 유연하고 확장 가능한 소프트웨어 디자인 🌟 feat. Java Example - 오픈소스컨설팅 테크블로그 %
지속 가능한 소프트웨어 설계 패턴: DDD + Hexagonal Architecture
주니어 개발자의 클린 아키텍처 맛보기 | 우아한형제들 기술블로그
멀티모듈 설계 이야기 with Spring, Gradle | 우아한형제들 기술블로그
Spring Boot Kotlin Multi Module로 구성해보는 헥사고날 아키텍처 | 우아한형제들 기술블로그