송금하기 예제를 사용하여 육각형 아키텍처를 직접적으로 반영하는 표현력 있는 패키지 구조 소개
결론: 간단한 구조의 계층은 가장 적합한 구조가 아닐 수 있다.
웹 계층, 도메인 계층, 영속성 계층의 각 계층 전용 패키지를 만든다.
payment
ㄴ web
ㄴ AccountController
ㄴ domain
ㄴ Account
ㄴ Activity
ㄴ AccountRepository
ㄴ AccountService
ㄴ persistence
ㄴ AccountRepositoryIml
기능 조각(functional slice)이나 특성(feature)로 구분짓는 경계가 없다.
만약에 사용자 기능이 추가된다면?
payment
ㄴ web
ㄴ AccountController
ㄴ UserController
ㄴ domain
ㄴ Account
ㄴ Activity
ㄴ User
ㄴ AccountRepository
ㄴ UserRepository
ㄴ AccountService
ㄴ UserService
ㄴ persistence
ㄴ AccountRepositoryImpl
ㄴ UserRepositoryImpl
연관되지 않은 클래스들의 엉망진창 묶음
유스케이스 파악이 어렵다.
특정 기능을 찾기 위해 AccountService
와 AccountController
로는 한 눈에 파악하기 힘들다. 추측과 찾는 시간이 필요하다.
imcoming port, outgoing port가 코드에 숨겨져 있다.
헥사고날 아키텍처를 따랐다고 가정했을 때, 웹 어댑터나 영속성 어댑터가 어느 기능을 호출하고 호출당하는지 파악할 수 없다,.
결론: 아키텍처의 가시성을 더 떨어뜨린다.
‘계층으로 구성하기’의 1번 문제를 해결한 방식
payment
ㄴ account
ㄴ Account
ㄴ Activity
ㄴ AccountRepository
ㄴ AccountRepositoryIml
ㄴ SendMoneyService
ㄴ AccountController
ㄴ user
ㄴ User
ㄴ UserRepository
ㄴ UserRepositoryIml
ㄴ UserRegistrationService
ㄴ UserController
모든 클래스들을 account라는 최상위 디렉토리에 위치시켰다.
package-private 접근 수준을 통해 패키지 경계를 만들어 불필요한 의존성을 방지한다.
AccountService
→ SendMoneyService
로 변경 (이는 앞에서도 변경 가능한 부분이었다)
Screaming Architecture(소리치는 아키텍처) - 클래스명(코드)만으로도 유스케이스(애플리케이션의 기능)를 찾을 수 있다.
헥사고날 아키텍처의 핵심 요소: 엔티티
, 유스케이스
, 인커밍/아웃고잉 포트
, 인커밍/아웃고잉 어댑터
위 핵심 요소들을 패키지 하나씩 매핑시킨다.
payment
ㄴ account
ㄴ adapter
ㄴ in
ㄴ web
ㄴ AccountController
ㄴ out
ㄴ persistence
ㄴ AccountPersistenceAdapter
ㄴ SpringDataAccountRepository
ㄴ domain
ㄴ Account
ㄴ Activity
ㄴ application (서비스 계층)
ㄴ SendMoneyService
ㄴ port
ㄴ in
ㄴ SendMoneyUseCase
ㄴ out
ㄴ LoadAccountPort
ㄴ UpdateAccountStatePort
ㄴ user
ㄴ adapter
ㄴ domain
ㄴ application
adapter 패키지의 모든 클래스들은 application/port 내의 인터페이스를 통하지 않고서는 호출되지 않기 때문에 package-private 접근 수준으로 설정한다. 애플리케이션 코어에서 바깥 계층으로 향하는 의존성을 막을 수 있다.
package com.woowa.cleanarchitecture.account.adapter.in.web;
@RequiredArgsConstructor
@RestController
@RequestMapping("/accounts")
class AccountController {
private final SendMoneyUseCase sendMoneyUseCase;
@PostMapping("/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(@PathVariable Long sourceAccountId, @PathVariable Long targetAccountId, @PathVariable Long amount) {
sendMoneyUseCase.sendMoney(sourceAccountId, targetAccountId, amount);
}
}
의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드(영속성/UI)에 의존하지 않게 하자!
애플리케이션 코드와 영속성 어댑터 같은 아웃고잉 어댑터는 제어 흐름과 의존성 방향이 반대이기 때문에 의존성 역전 원칙을 사용해야 한다.
따라서 이 두 계층 사이에 아웃고잉 포트 인터페이스가 필요하다.
‘모든 계층에 의존성을 가진 중립적인 컴포넌트’(ex. 스프링 프레임워크)가 실제 객체를 필요한 다른 계층에 제공한다. (의존성을 주입한다.)
코드에서 아키텍처의 특정 요소를 찾기 위해선 아키텍처 다이어그램의 박스 이름을 따라 탐색하면 된다. (챕터2-5 육각형 아키텍처 다이어그램 참고)
의사소통, 개발, 유지보수 모두가 수월해질 것이다.
p29 1문단 adapter package: 이 패키지의 모든 클래스들은 application 패키지 내에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출되지 않기 때문에 package-private 접근 수준으로 둬도 된다.
→ 의존성 역전으로 구현하였기 때문에 인터페이스를 통해 어댑터 구현체가 호출된다.
P27 SendMoneyService는 인커밍 포트 인터페이스인 SendMoneyUseCase를 구현하고 ~
: SendMoneyService implements SendMoneyUseCase
p29 3문단 어댑터 코드를 자체 패키지로 이동(?)시키면 필요할 경우 하나의 어댑터를 다른 구현으로 쉽게 교체할 수 있다는 장점도 있다.