Book/DEV

만들면서 배우는 클린 아키텍쳐 (2)

코드파고 2025. 3. 14. 13:55

만들면서 배우는 클린 아키텍쳐 (톰 홀버그 지음, 위키북스)를 읽고 정리한 내용입니다.

 

이전 포스팅


포트

핵심 비즈니스 로직과 외부 시스템 간의 상호작용을 정의하는 인터페이스 역할을 하는 컴포넌트

입력 포트 (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로 구성된 패키지이다.

패키지 구조