CAP for Java

CAP 개발자 80% 모르는 Custom Action 핸들러 #shorts #SAP #CAP

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

아직 댓글이 없습니다.