Spring Web MVC 를 이용해 개발다보면 대부분 controller - service - dao 구조로 잡혀있다는것을 쉽게 볼 수 있다.
controller(web) 는 http 요청을 받아 service 로 전달하고, 또 service 에서 리턴된 결과를 http 응답으로 전달한다.
service(domain) 는 비지니스 로직을 수행해 dao 로 전달하고, 또 dao 에서 리턴된 결과를 갖고 비지니스 로직을 수행한다.
dao(persistence) 는 service 에서 만들어진 데이터를 db 같은 persistence 영역으로 보내는 역할을 수행한다.
명확하게 딱딱 나눠져 있어 보이지만, 실제 개발하다 보면 그 경계가 모호해질 때도 있고 실수로 다른 계층으로 해야할일을 넘길 수 있다.
Get Your Hands Dirty on Clean Architecture 책은 그에 대한 해결책을 제시했고 그부분을 정리했다.
먼저 아래 그림을 보자.
저자는 Hexagonal Architecutre 라고 네이밍 지었고 육각형 모양이 갖는 의미는 크게 없다.
중요한 부분은 Port 라는 개념이다. Web Adapter(controller) 나 Persistence Adapter(dao) 그리고 다른 External System Adapter 들이 Use Case(service) 로 요청을 보내기 위해선 Port 를 거쳐야 한다. 다시 말해 Port 들이 Adapter 와 Use Case 간의 경계를 구분지어 준다.
좌측 상단 Web Adapter - Input Port - Use Case 관계를 자세히 보자.
SOLID 원칙에서 D 에 해당하는 Dependency Inversion Principle (의존성 역전 원칙) 이 적용되어 있는데 아래 그림을 통해 DIP 에 대해 먼저 간단히 알아보자. 만약 A 와 B 가 직접적으로 Compile time 의존성이 있었다면 B가 변함에 따라 A도 바로 영향을 받게 된다. 하지만 B 는 거꾸로 I 에 의존성이 있고(B 는 I의 구현체) A는 I 를 통해 접근한다면 B 의 Compile time 의존성은 Runtime 의존성과 반대가 된다.
의존성을 갖는 부분을 추상화함으로써 경계를 짓는다. Web Adapter - Input Port - Use Case 관계도 이와 같다.
Use Case(service) 는 Input Port 만을 의존하기 때문에 Web Adapter 를 직접적으로 몰라도 된다.
이제 계좌이체 기능을 구현하는 account 패키지를 보면서 구체적으로 알아보자.
account 패키지 아래 adapter(in/out), application 패키지 안에 port(in/out) 과 service 그리고 domain 패키지가 있다.
adapter 와 domain 은 각 역할이 명확하게 그려지는데 application 패키지 안에 port, service 는 위 구조만 봐서는 잘 모르겠다.
계좌이체를 구현한 SendMoney flow 를 한번 자세히 알아보자.
SendMoneyController(Adapter)는 SendMoneyUseCase(Port) 의존성을 갖는다. 클래스이름은 Use Case 이지만 실제로 하는 역할은 Port 이니 헷갈리지 말자. SendMoneyService(Use Case) 는 SendMoneyUseCase 인터페이스를 구현한 구현체이고 아까 DIP 그림에서와 같이 의존성 역전 관계에 있다.
그리고 Http request path variable 을 파싱해서 SendMoneyCommand 객체에 담아 SendMoneyService(Use Case) 로 전달하는데 이 SendMoneyCommand 도 Port 로써 역할을 수행한다.
package io.reflectoring.buckpal.account.adapter.in.web;
import io.reflectoring.buckpal.account.application.port.in.SendMoneyUseCase;
import io.reflectoring.buckpal.account.application.port.in.SendMoneyCommand;
import io.reflectoring.buckpal.common.WebAdapter;
import io.reflectoring.buckpal.account.domain.Account.AccountId;
import io.reflectoring.buckpal.account.domain.Money;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@WebAdapter
@RestController
@RequiredArgsConstructor
class SendMoneyController {
private final SendMoneyUseCase sendMoneyUseCase;
@PostMapping(path = "/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId,
@PathVariable("targetAccountId") Long targetAccountId,
@PathVariable("amount") Long amount
) {
SendMoneyCommand command = new SendMoneyCommand(
new AccountId(sourceAccountId),
new AccountId(targetAccountId),
Money.of(amount));
sendMoneyUseCase.sendMoney(command);
}
}
그리고 SendMoneyController(Adapter)는 Money, AccountId 같은 Entity 를 직접 의존해도 된다. 하지만 반대는 안된다.
원문 자료 : Get Your Hands Dirty on Clean Architecture(https://reflectoring.io/spring-hexagonal/)
'Spring' 카테고리의 다른 글
WebClient 사용할때 주의 (2편) (0) | 2021.06.10 |
---|---|
request body 한번만 읽어올수 있는 제약 (0) | 2021.06.09 |
Spring Cloud Gateway CORS 주의사항 (4) | 2021.01.24 |
useInsecureTrustManager 옵션 (0) | 2021.01.20 |
request body memory leak (0) | 2021.01.20 |