헥사고날 (hexagonal-springboot)
architecture = 'hexagonal' 은 DDD 읽기에 육각형의 들어오는 쪽을 더합니다: 노출된 애그리거트 루트마다
유스케이스 인터페이스가 생기고, 응용 서비스가 그것을 구현하고, 컨트롤러는 구현 클래스가 아니라 인터페이스를 주입받습니다.
application/port/in/OrderUseCase <- 애플리케이션이 밖에서 불리는 면
application/OrderService <- implements OrderUseCase (내 확장 서비스)
adapter/in/web/OrderController <- OrderUseCase 를 주입받음 (구현 클래스가 아니라)
포트의 메서드 목록은 생성된 서비스의 면을 그대로 옮긴 것이라, "서비스가 포트를 구현한다" 는 것을 컴파일러가 빌드마다
검사합니다. @restExclude(...) 로 뺀 메서드는 유스케이스에서도 빠집니다.
두 방향
| 방향 | 무엇 | 네임스페이스 | 어댑터 |
|---|---|---|---|
| 인바운드 | OrderUseCase - 애플리케이션에 들어가는 문 | inboundPort (대체: service) | 컨트롤러 (생성됨) |
| 아웃바운드 (영속) | OrderRepositoryPort - 도메인이 소유 | domainRepository (대체: entity) | OrderRepositoryAdapter (생성됨) |
| 아웃바운드 (SQL/HTTP) | port: DSL | port | 런타임 프록시 (생성물 없음) |
Pet Store 에서
Pet Store 예제는 Spring Boot 4 위의 헥사고날 Java 입니다. env.yml 은 base 패키지 하나만 적습니다:
namespace:
base: io.elasticore.sample.petstore
PurchaseOrder 의 states: 가 유스케이스, 컨트롤러 엔드포인트, Base 의 메서드가 되었습니다:
public interface PurchaseOrderUseCase {
PurchaseOrderDTO save(PurchaseOrderDTO dto);
Optional<PurchaseOrderDTO> findById(String orderId);
Page<PurchaseOrderDTO> findBySearch(PurchaseOrderSrchDTO searchDTO);
List<Boolean> delete(List<PurchaseOrderKeyDTO> dtoList);
boolean deleteById(String orderId);
PurchaseOrderDTO ship(String orderId);
PurchaseOrderDTO cancel(String orderId);
}
@Operation(summary = "PurchaseOrder: ship")
@PostMapping("/{orderId}/ship")
public PurchaseOrderDTO ship(@PathVariable("orderId") String orderId) {
return purchaseOrderService.ship(orderId);
}
public void ship() {
moveStatus("ship", OrderStatus.SHIPPED, OrderStatus.PLACED);
raiseShipped();
}
/** Whether ship may be given now: the status is PLACED. */
public boolean canShip() {
return this.status == OrderStatus.PLACED;
}
PurchaseOrder 에는 @editable 필드가 없어서 유스케이스에 update 가 없습니다 - 주문은 명령으로만 바뀝니다.
확장 서비스는 내 파일이고 유스케이스를 구현합니다. 생성자는 포트 둘과 EntityManager 를 받습니다:
@Service
public class PurchaseOrderService extends PurchaseOrderCoreService
implements io.elasticore.sample.petstore.application.port.in.PurchaseOrderUseCase {
public PurchaseOrderService(PurchaseOrderRepositoryPort repository,
io.elasticore.sample.petstore.application.port.out.PurchaseOrderQueryPort query,
jakarta.persistence.EntityManager entityManager,
CustomerRepositoryPort customers, /* ... */) {
super(repository, query, entityManager);
// ...
}
}
패키지 배치
namespace:
entity: com.acme.shop.domain.model
enumeration: com.acme.shop.domain.model
event: com.acme.shop.domain.event
domainService: com.acme.shop.domain.service
domainRepository: com.acme.shop.domain.repository # <Root>RepositoryPort
repository: com.acme.shop.infrastructure.persistence # Spring Data
persistence: com.acme.shop.infrastructure.persistence # 두 포트를 구현하는 어댑터
service: com.acme.shop.application
dto: com.acme.shop.application.dto
inboundPort: com.acme.shop.application.port.in # <Root>UseCase
queryPort: com.acme.shop.application.port.out # <Root>QueryPort
port: com.acme.shop.application.port.out # port: DSL 인터페이스
control: com.acme.shop.adapter.in.web
port: 인터페이스는 애플리케이션의 것입니다 - 계약이 application DTO 로 쓰이기 때문입니다. SQL/HTTP 아웃바운드
포트가 생성하는 파일은 인터페이스 하나이고, 어댑터는 런타임 프록시입니다.
in 패키지adapter.in.web 의 in 은 Kotlin 예약어입니다. Kotlin 출력에서는 패키지·import·정규화 이름의 in 을
`in` 으로 감쌉니다. 직접 쓰는 Kotlin 파일도 package com.acme.adapter.`in`.web 로 씁니다.
언제 고르나
새 프로젝트의 기본입니다 - Pet Store, README 의 시작하기, 에이전트 키트의 레시피가 모두 씁니다. 유스케이스
인터페이스가 필요 없으면 ddd 를 고릅니다.
예제: elasticore-sample-petstore
(Java, Spring Boot 4), elasticore-template-ddd
(Java, architecture = 'hexagonal'),
elasticore-template-kt 의
shop 도메인 (hexagonal-springboot-kt).