주문(Order) 기능의 흐름
“사용자(Web)가 책을 주문(place order)한다.” 라는 하나의 유스케이스가 어떻게 각 계층을 통과하는지 따라가 보겠습니다.
1. 인바운드(Inbound) 흐름: 외부 요청이 비즈니스 로직으로 들어오는 과정
외부 요청자(사용자) → 웹 어댑터(Controller) → 인바운드 포트(UseCase) → 서비스(UseCase 구현체)
PlaceOrderController.java(인바운드 어댑터)- 위치:
adapter/in/web/ - 역할: 사용자의 HTTP
POST /orders요청을 받습니다. JSON 요청 본문(PlaceOrderRequest)을 파싱합니다. - 코드의 핵심:
123456789101112@RestControllerpublic class PlaceOrderController {private final PlaceOrderUseCase placeOrderUseCase; // ✅ 인바운드 포트에 의존@PostMapping("/orders")public void placeOrder(@RequestBody PlaceOrderRequest request) {// ... DTO를 Command 객체 등으로 변환 ...placeOrderUseCase.placeOrder(command); // ✅ "주문해줘" 라고 포트에 명령}}
- 위치:
PlaceOrderUseCase.java(인바운드 포트)- 위치:
application/port/in/ - 역할: “주문하기” 기능의 명세서(스펙)입니다. “주문하기”라는 행위가 존재함을 선언합니다.
- 코드의 핵심:
12345public interface PlaceOrderUseCase {Order placeOrder(PlaceOrderCommand command); // ✅ "주문하기" 기능 정의}1<span class="line"><span></span></span>
- 위치:
PlaceOrderService.java(서비스, 유스케이스 구현체)- 위치:
application/service/ - 역할:
PlaceOrderUseCase인터페이스를 실제로 구현합니다. 주문 생성과 관련된 실제 비즈니스 로직을 수행합니다. - 코드의 핵심:
12345678910111213141516@Servicepublic class PlaceOrderService implements PlaceOrderUseCase { // ✅ 인바운드 포트 구현private final SaveOrderPort saveOrderPort; // ✅ 아웃바운드 포트에 의존@Overridepublic Order placeOrder(PlaceOrderCommand command) {// 1. 재고 확인 로직 (필요 시 다른 포트 호출)// 2. 총 주문 금액 계산 로직Order newOrder = Order.create(command); // 3. 순수 도메인 객체 생성return saveOrderPort.save(newOrder); // 4. "이 주문 저장해줘" 라고 포트에 요청}}
- 위치:
2. 아웃바운드(Outbound) 흐름: 비즈니스 로직이 외부 기술을 사용하는 과정
서비스(UseCase 구현체) → 아웃바운드 포트(Port) → 영속성 어댑터(Persistence Adapter) → 외부(DB)
PlaceOrderService.java(서비스, 유스케이스 구현체)- 위에서 본
PlaceOrderService는 주문을 생성한 후, 이를 저장하기 위해 아웃바운드 포트인SaveOrderPort를 호출합니다. PlaceOrderService는SaveOrderPort가 어떻게 구현되었는지(JPA인지, MyBatis인지) 전혀 알지 못합니다. 그저 “저장”이라는 기능이 존재하고 호출할 수 있다는 것만 압니다.
- 위에서 본
SaveOrderPort.java(아웃바운드 포트)- 위치:
application/port/out/ - 역할: 데이터베이스에 “주문을 저장하는” 기능의 명세서입니다. 애플리케이션이 외부(영속성 계층)에 무엇을 요구하는지 정의합니다.
- 코드의 핵심:
12345public interface SaveOrderPort {Order save(Order order); // ✅ "주문 저장" 기능 정의}
- 위치:
OrderPersistenceAdapter.java(영속성 어댑터, 아웃바운드 포트 구현체)- 위치:
adapter/out/persistence/ - 역할:
SaveOrderPort인터페이스를 실제로 구현합니다. JPA,OrderJpaEntity,OrderJpaRepository와 같은 실제 DB 기술을 사용하여 데이터를 저장합니다. - 코드의 핵심:
12345678910111213141516171819@Repository // Spring Component로 등록public class OrderPersistenceAdapter implements SaveOrderPort { // ✅ 아웃바운드 포트 구현private final OrderJpaRepository orderJpaRepository; // ✅ 실제 DB 기술에 의존@Overridepublic Order save(Order order) {// 1. 순수 도메인 객체(Order)를 DB 저장을 위한 JPA 엔티티(OrderJpaEntity)로 변환OrderJpaEntity entity = OrderJpaEntity.fromDomain(order);// 2. Spring Data JPA를 사용하여 DB에 실제로 저장OrderJpaEntity savedEntity = orderJpaRepository.save(entity);// 3. 저장된 엔티티를 다시 순수 도메인 객체로 변환하여 반환return savedEntity.toDomain();}}
- 위치:
요약: 소스 이름으로 보는 관계
| 단계 | 파일명 (역할) | 계층 | 설명 |
|---|---|---|---|
| 1 | PlaceOrderController.java |
Inbound Adapter | HTTP 요청을 받음 |
| 2 | PlaceOrderUseCase.java (인터페이스) |
Inbound Port | 컨트롤러가 호출할 기능(placeOrder)을 정의 |
| 3 | PlaceOrderService.java |
Service | PlaceOrderUseCase를 구현. 비즈니스 로직 수행 |
| 4 | SaveOrderPort.java (인터페이스) |
Outbound Port | 서비스가 DB에 요청할 기능(save)을 정의 |
| 5 | OrderPersistenceAdapter.java |
Outbound Adapter | SaveOrderPort를 구현. 실제 JPA 코드로 DB에 저장 |
이처럼 각 파일의 이름과 위치는 그 파일의 역할과 책임을 명확하게 나타내도록 짓는 것이 헥사고날 아키텍처를 유지하는 데 매우 중요합니다.