만들면서 배우는 클린 아키텍쳐 (톰 홀버그 지음, 위키북스)를 읽고 정리한 내용입니다.
이전 포스팅
포트
핵심 비즈니스 로직과 외부 시스템 간의 상호작용을 정의하는 인터페이스 역할을 하는 컴포넌트
입력 포트 (Input Port)
- 유스케이스의 진입점 역할
- 외부에서 내부로 흐르는 데이터 흐름 (=외부 요청을 내부로 전달)
- Controller와 같은 입력 어댑터에서 유스케이스를 호출할 때 사용
- 애플리케이션의 핵심 비즈니스 로직을 담고 있는 서비스(Service)가 이 인터페이스를 구현
- 비즈니스 로직의 공개 API
예시
public interface SendMoneyUseCase {
boolean sendMoney(SendMoneyCommand command);
}
출력 포트 (Output Port)
- 도메인 엔티티가 외부 시스템(데이터베이스, 메시지 브로커, API)과 통신하도록 돕는 역할
- 내부에서 외부로 흐르는 데이터 흐름 (= 내부 처리 결과를 외부로 전달)
- Persistence Adapter 같은 출력 어댑터에서 이 인터페이스를 구현
- 외부 시스템과의 의존성을 캡슐화하는 역할
예시
public interface LoadAccountPort {
Account loadAccount(AccountId accountId, LocalDateTime baselineDate);
}
public interface UpdateAccountStatePort {
void updateActivities(Account account);
}
어댑터
외부 시스템과 애플리케이션 코어(유스케이스) 사이를 연결하는 역할을 수행한다.
포트 인터페이스를 실제로 구현하여 외부 시스템과 상호 작용한다.
웹 어댑터 (Web Adapter)
웹 인터페이스를 제공하는 어댑터이며, 주도하는/인커밍 어댑터이다.
대표적으로 컨트롤러가 있다.
역할
- HTTP 객체 매핑
- 권한 검사
- 입력 유효성 검증 및 매핑
- 유스케이스 호출
- 출력을 HTTP로 매핑 및 반환
동작 과정
컨트롤러(웹 어댑터) ➡️ 포트(어플리케이션) ➡️ 서비스(어플리케이션)
더하자면, 포트가 생략되어 [컨트롤러 -> 서비스]로 작동 가능하다는 점에서 의존성 역전의 법칙 이 성립한다.
예시 - 컨트롤러
@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);
}
}
영속성 어댑터 (Persistence Adapter)
주도되는 / 아웃고잉 어댑터
애플리케이션 계층과 외부 영속성 시스템(데이터베이스, 파일 시스템 등) 간의 상호 작용을 처리
데이터베이스 쿼리, 파일 입출력 등 영속성 관련 기술적인 세부 사항을 처리하며, 포트를 통해 서비스와 상호작용
동작 과정
- 서비스로부터 데이터 처리 / 조회 요청 수신
- 요청 데이터를 영속성 시스템에 적합한 형식으로 변환
- 영속성 시스템과 상호 작용하여 데이터를 저장하거나 조회
- 영속성 시스템으로부터 결과를 수신
- 수신된 결과를 애플리케이션 계층에서 사용할 수 있는 형식으로 변환하여 출력 포트를 통해 서비스로 전달
- 출력 코어는 어플리케이션 코어에 위치함을 유의하자
예시
@RequiredArgsConstructor
class AccountPersistenceAdapter implements LoadAccountPort, UpdateAccountStatePort { // 포트 구현
private final SpringDataAccountRepository accountRepository;
private final ActivityRepository activityRepository;
private final AccountMapper accountMapper;
@Override
// Account 모델은 Entity가 아닌 도메인 객체
public Account loadAccount(
AccountId accountId,
LocalDateTime baselineDate) {
...
}
@Override
public void updateActivities(Account account) {
...
}
}
위의 어댑터는 service 계층에서 사용된다.
@Transactional
@RequiredArgsConstructor
public class SendMoneyService implements SendMoneyUseCase {
private final LoadAccountPort loadAccountPort;
...
@Override
public boolean sendMoney(SendMoneyCommand command) {
LocalDateTime baselineDate = LocalDateTime.now().minusDays(10);
Account sourceAccount = loadAccountPort.loadAccount(
command.getSourceAccountId(),
baselineDate);
Account targetAccount = loadAccountPort.loadAccount(
command.getTargetAccountId(),
baselineDate);
...
}
}
유스케이스 (Use Case)
애플리케이션의 핵심 비즈니스 로직을 담당하는 계층
이 계층은 비즈니스 로직을 구체적으로 어떻게 실행할지 정의하며, 캡슐화된 서비스 계층이라고 이해할 수 있다.
동작 과정
입력을 받음 ➡️ 모델의 상태 변경 ➡️ 영속성 어댑터를 통해 포트로 상태 전달 ➡️ 저장 ➡️ 출력값을 출력 객체로 변환
예시
입력 포트(인터페이스)
public interface SendMoneyUseCase {
boolean sendMoney(SendMoneyCommand command);
}
유스케이스(입력 포트를 구현한 서비스)
@Transactional
@RequiredArgsConstructor
public class SendMoneyService implements SendMoneyUseCase {
private final LoadAccountPort loadAccountPort; // persistence-out port
private final AccountLock accountLock;
private final UpdateAccountStatePort updateAccountStatePort; // persistence-out port
@Override
public boolean sendMoney(SendMoneyCommand command) {
...
}
}
정리 및 패키지 구조
클래스가 어디에 위치하는지 확인하며 정리해보자!
크게 application, domain, adapter, config, common 로 나뉘며,
application은 비지니스 로직의 실질적인 구현인 service와, 명세에 해당하는 port가 위치하는 패키지
domain은 비지니스 모델이 상호작용하는 순수 자바 객체가 위치하는 곳
adapter는 외부와 상호작용하는 컴포넌트인 web-adapter들,
DB와 같은 외부 시스템과 소통하는 persistence-adapter로 구성된 패키지이다.
'Book > DEV' 카테고리의 다른 글
만들면서 배우는 클린 아키텍쳐 (1) | 2024.12.11 |
---|---|
Effective Java - 상속과 합성 (0) | 2022.11.28 |