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

개발자 소유 서비스에 쓰는 로직

모델은 애플리케이션이 무엇인지 말합니다. 모델이 말할 수 없는 것은 생성기가 넘겨준 파일에 씁니다 - 무엇보다 재생성되는 XxxCoreService 를 상속하는 확장 서비스 XxxService. 둘째 줄이 // Initially generated by ElastiCORE; 로 시작하는 파일이라, 한 번 고치면 내 것이 됩니다.

무엇을어디에
쓰기에 대한 규칙 - 다른 값에서 정해지는 값, 다른 행을 보는 검사XxxService 의 beforeSave
로그인 규칙 (계정 잠금)authenticate(login, password) override
생명주기 이동에 더 할 일 (택배사 호출)XxxService 에서 그 명령 override (ship(orderId))
애그리거트 안의 규칙 (상태 확인, 이벤트)나뉜 루트의 행위 클래스 Xxx
CRUD 에 없는 연산 (대출 반납, 보고서)XxxService 의 메서드 + 작은 컨트롤러 하나
생성된 리포지터리의 커스텀 쿼리보존 블록 하나

컨트롤러, DTO, 리포지터리, 보안, 로그인, 오류 처리기는 손으로 쓰지 마세요 - 생성됩니다. 메서드는 첫 생성으로 XxxService.java 가 생긴 뒤에 거기 더합니다. 그 파일을 생성 전에 직접 쓰지 않습니다.

beforeSave - 행을 쓰는 모든 경로​

코어 서비스는 요청 값이 엔티티에 들어간 뒤, 저장 직전에 부릅니다 - 생성, PUT /, PUT /batch, 조건 수정의 네 경로 모두.

훅언제
beforeSave(entity)엔티티만 보고 끝낼 일: 합계 재계산, 상태 찍기
beforeSave(entity, request)요청이 무엇을 실어 왔는지 알아야 할 일

기본 beforeSave(entity, request) 가 beforeSave(entity) 를 부르므로 하나만 override 하면 됩니다.

Pet Store 는 새 주문이 처음 저장될 때 주문을 접수합니다:

application/PurchaseOrderService.java (Pet Store)
@Override
protected void beforeSave(PurchaseOrder order) {
if (order.getOrderDate() == null) {
order.place(LocalDate.now(clock), this::homeOf, vatRate, discountRate);
}
}

상점의 정책 자체는 루트에 있습니다:

domain/model/PurchaseOrder.java (Pet Store, 줄임)
public void place(LocalDate today, Function<Long, Address> homeOf, BigDecimal vatRate, BigDecimal discountRate) {
if (getOrderDate() != null) throw new InvariantViolationException("order " + getOrderId() + " is already placed");
if (getDeliveryAddress() == null) setDeliveryAddress(homeOf.apply(getCustomerId()));
// ...

Money withoutVat = getTotalWithoutVat(); // 줄의 @compute 합
Money vat = percent(withoutVat, vatRate);
Money discount = percent(withoutVat.plus(vat), discountRate);

setOrderDate(today);
setVat(vat);
setDiscount(discount);
setTotal(withoutVat.plus(vat).minus(discount).rounded(2));
raisePlaced();
}

"처음 한 번만" 은 상태로 구분합니다 - @sequence 면 아직 null 인 ID, 위처럼 비어 있는 날짜. 행을 만들 때의 시각은 코드가 필요 없습니다: @readonly @default(now).

거절하기​

런타임의 예외를 던지면 생성된 오류 처리기가 내 메시지를 detail 로 답합니다 - API 와 오류 응답:

throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn);   // 409
throw new BadRequestException("..."); // 400

entityManager 는 서비스의 필드입니다: entityManager.find(Book.class, isbn) 로 행을 읽고, entityManager.createQuery("select count(l) from Loan l where ...", Long.class) 로 셉니다.

update() override 와 PUT /batch​

3.0.261022-rc1 부터 batchUpdate(PUT /batch)는 한 트랜잭션 안에서 행마다 update(dto) 를 부르므로, update() override 의 규칙이 일괄 수정에도 같은 검사·409 변환과 함께 적용됩니다. 전에는 일괄 수정이 update() 의 본문을 복사해 그런 override 를 건너뛰었습니다. 쓰기에 대한 규칙은 여전히 모든 경로가 지나는 beforeSave 가 제자리입니다.

직접 만든 연산​

서비스의 메서드와 작은 컨트롤러 하나입니다. 에이전트 키트의 상점 레시피에서 - 관리자 보고서:

application/PurchaseOrderService.java - 추가
    /** Orders per status and the revenue of those not cancelled. "report" is not in the entity's
* @access, so it is the administrator's: 401 for a visitor, 403 for a member. */
@org.springframework.transaction.annotation.Transactional(readOnly = true)
public java.util.Map<String, Object> report() {
ACCESS.require(currentUser, "report");
java.util.Map<String, Long> counts = new java.util.LinkedHashMap<>();
java.math.BigDecimal revenue = java.math.BigDecimal.ZERO;
for (Object[] row : entityManager.createQuery(
"select o.status, count(o), coalesce(sum(o.total), 0) from PurchaseOrder o group by o.status",
Object[].class).getResultList()) {
com.acme.store.domain.model.OrderStatus status = (com.acme.store.domain.model.OrderStatus) row[0];
counts.put(status.name(), (Long) row[1]);
if (status != com.acme.store.domain.model.OrderStatus.CANCELLED) {
revenue = revenue.add(new java.math.BigDecimal(String.valueOf(row[2])));
}
}
java.util.Map<String, Object> report = new java.util.LinkedHashMap<>();
report.put("countByStatus", counts);
report.put("revenue", revenue.setScale(2, java.math.RoundingMode.HALF_UP));
return report;
}
adapter/in/web/OrderReportController.java - 새 내 파일
@RestController
@RequestMapping("/api/store/purchaseOrder")
public class OrderReportController {

private final PurchaseOrderService service;

public OrderReportController(PurchaseOrderService service) {
this.service = service;
}

@GetMapping("/report")
public Map<String, Object> report() {
return service.report();
}
}

ACCESS(엔티티의 @access), currentUser, entityManager 는 생성된 서비스의 필드입니다. 컨트롤러는 생성된 컨트롤러와 같은 패키지에 두어야 생성된 오류 처리기가 그 401·403 을 답합니다. 다음 빌드는 서비스를 SKIP(user-owned) 로 보고하고 메서드를 남깁니다.

루트 안: 나뉜 루트 (DDD 번들)​

루트의 생명주기 이동은 생성됩니다. 더 할 일은 행위 클래스에서 super 와 함께:

public class PurchaseOrder extends PurchaseOrderBase {
@Override
public void ship() {
super.ship(); // 상태 확인 + 이동 + 이벤트
// 그다음 할 일
}
}

생명주기가 없는 commands: 명령은 Base 의 추상 메서드라, 행위 클래스에 쓸 때까지 컴파일되지 않습니다.

그 밖의 내 파일​

  • 컨트롤러 - 한 번만 생성. 손댄 컨트롤러에는 새 엔드포인트(새 명령, @signin)가 들어가지 않고, 빌드가 빠진 것을 경고합니다.
  • XxxApiErrors - 오류 응답의 형식.
  • 도메인 서비스, 프로세스 매니저, 팩토리 - Base 는 다시 생성되고 내 클래스는 그대로.
  • 애플리케이션 클래스, 시작 데이터, 이벤트 리스너 - 평범한 내 코드.