본문으로 건너뛰기
ElastiCORE 3.0 은 릴리스 후보(RC)입니다: Maven Central 의 3.0.261022-rc1. 2.2 라인 프로젝트는 2.2 문서를 보세요.
버전: 3.0

헥사고날 (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: DSLport런타임 프록시 (생성물 없음)

Pet Store 에서​

Pet Store 예제는 Spring Boot 4 위의 헥사고날 Java 입니다. env.yml 은 base 패키지 하나만 적습니다:

blueprint/petstore/env.yml
  namespace:
base: io.elasticore.sample.petstore

PurchaseOrder 의 states: 가 유스케이스, 컨트롤러 엔드포인트, Base 의 메서드가 되었습니다:

application/port/in/PurchaseOrderUseCase.java (생성)
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);
}
adapter/in/web/PurchaseOrderController.java (한 번만 생성)
    @Operation(summary = "PurchaseOrder: ship")
@PostMapping("/{orderId}/ship")
public PurchaseOrderDTO ship(@PathVariable("orderId") String orderId) {
return purchaseOrderService.ship(orderId);
}
domain/model/PurchaseOrderBase.java (재생성)
	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 를 받습니다:

application/PurchaseOrderService.java (내 것)
@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);
// ...
}
}

패키지 배치​

env.yml - 헥사고날 프리셋 (namespace: base 가 채우는 것)
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 아웃바운드 포트가 생성하는 파일은 인터페이스 하나이고, 어댑터는 런타임 프록시입니다.

Kotlin 과 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).