CAP Java Custom Action의 핵심 동작 원리
CAP for Java에서 Custom Action을 처음 구현하는 개발자의 80%가 모르는 점이 있습니다. Action 핸들러는 단순히 메서드를 등록하는 것이 아니라 CAP의 이벤트 처리 파이프라인에 연결됩니다. @Before, @On, @After의 차이, 파라미터 접근 방법, 트랜잭션 처리가 어떻게 동작하는지 알아야 올바르게 구현할 수 있습니다.
이벤트 파이프라인: Before → On → After
// CDS 서비스
service InvoiceService {
entity Invoices as projection on db.Invoices;
action approveInvoice(invoiceId: String, approver: String) returns Invoices;
}
@Component
@ServiceName("InvoiceService")
public class InvoiceHandler implements EventHandler {
// Before: 입력 검증 (On 실행 전)
@Before(event = "approveInvoice", service = "InvoiceService")
public void validateApproval(ActionEventContext ctx) {
String invoiceId = (String) ctx.getParameterInfo().getParam("invoiceId");
String approver = (String) ctx.getParameterInfo().getParam("approver");
if (invoiceId == null || invoiceId.isEmpty()) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "invoiceId 필수");
}
// DB에서 전표 존재 확인
var result = persistenceService.run(
Select.from(Invoices_.class).byId(invoiceId)
);
if (result.rowCount() == 0) {
throw new ServiceException(ErrorStatuses.NOT_FOUND,
"전표 " + invoiceId + " 없음");
}
var invoice = result.single(Invoices.class);
if ("APPROVED".equals(invoice.getStatus())) {
throw new ServiceException(ErrorStatuses.CONFLICT,
"이미 승인된 전표입니다.");
}
}
// On: 실제 비즈니스 로직 (핵심 처리)
@On(event = "approveInvoice", service = "InvoiceService")
public void doApproval(ActionEventContext ctx) {
String invoiceId = (String) ctx.getParameterInfo().getParam("invoiceId");
String approver = (String) ctx.getParameterInfo().getParam("approver");
// 상태 업데이트
persistenceService.run(
Update.entity(Invoices_.class)
.data(Map.of(
"status", "APPROVED",
"approvedBy", approver,
"approvedAt", LocalDateTime.now().toString()
))
.byId(invoiceId)
);
// 업데이트된 전표 반환
var updated = persistenceService.run(
Select.from(Invoices_.class).byId(invoiceId)
);
ctx.setResult(updated.single(Invoices.class));
}
// After: 후처리 (알림, 로그 등)
@After(event = "approveInvoice", service = "InvoiceService")
public void afterApproval(ActionEventContext ctx) {
// 결과에서 승인된 전표 읽기
var invoice = ctx.getResult().as(Invoices.class);
// 알림 발송 (이메일, 텔레그램 등)
notificationService.sendApprovalNotice(
invoice.getInvoiceId(),
invoice.getApprovedBy()
);
// 감사 로그 기록
auditLogger.log("INVOICE_APPROVED", invoice.getInvoiceId());
}
}
복잡한 파라미터 처리
// CDS: 구조체 파라미터
service OrderService {
type OrderLineInput {
productId : String;
quantity : Integer;
unitPrice : Decimal;
}
action createBulkOrder(
customerId : String,
lines : many OrderLineInput
) returns Orders;
}
// Java: 복잡한 파라미터 읽기
@On(event = "createBulkOrder", service = "OrderService")
public void createBulkOrder(ActionEventContext ctx) {
var params = ctx.getParameterInfo();
String customerId = (String) params.getParam("customerId");
// 배열 파라미터는 List>로 읽힘
@SuppressWarnings("unchecked")
List> lines =
(List>) params.getParam("lines");
if (lines == null || lines.isEmpty()) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "주문 항목 필요");
}
// 주문 헤더 생성
var orderData = Orders.create();
orderData.setCustomerId(customerId);
orderData.setStatus("DRAFT");
orderData.setOrderDate(LocalDate.now().toString());
String newOrderId = persistenceService.run(
Insert.into(Orders_.class).entry(orderData)
).single(Orders.class).getOrderId();
// 주문 항목 일괄 생성
var lineEntries = lines.stream().map(line -> {
var item = OrderItems.create();
item.setOrderId(newOrderId);
item.setProductId((String) line.get("productId"));
item.setQuantity(((Number) line.get("quantity")).intValue());
item.setUnitPrice((BigDecimal) line.get("unitPrice"));
return item;
}).collect(Collectors.toList());
persistenceService.run(Insert.into(OrderItems_.class).entries(lineEntries));
// 생성된 주문 반환
ctx.setResult(persistenceService.run(
Select.from(Orders_.class).byId(newOrderId)
).single(Orders.class));
}
트랜잭션: CDS가 자동으로 관리
// CAP Java에서 Action 내부의 모든 DB 작업은
// 자동으로 하나의 트랜잭션으로 묶임
// @On 핸들러가 예외 없이 완료되면 → 자동 커밋
// 예외가 발생하면 → 자동 롤백
@On(event = "createBulkOrder", service = "OrderService")
public void createBulkOrder(ActionEventContext ctx) {
// 이 두 Insert는 같은 트랜잭션
persistenceService.run(Insert.into(Orders_.class).entry(header));
persistenceService.run(Insert.into(OrderItems_.class).entries(items));
// 예외 발생 시 두 Insert 모두 롤백됨
if (someValidationFails) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "검증 실패");
// → header Insert와 items Insert 모두 취소됨
}
}
공식 문서
CAP Java 이벤트 핸들러와 Action 구현 전체 가이드는 cap.cloud.sap/docs/java/event-handlers에서 확인하세요.
댓글 0
아직 댓글이 없습니다.