CAP for Java

CAP 영속성 API — 개발자 90%가 놓친다 #shorts #SAP #CAPforJava

▶ YouTube에서 보기

CAP PersistenceService란 무엇인가

CAP(Cloud Application Programming Model)에서 데이터베이스에 접근하는 가장 일반적인 방법은 cds.connect.to('db')로 DatabaseService를 얻어 쿼리를 실행하는 것입니다. 그런데 이 방식은 CAP의 이벤트 핸들러 파이프라인을 우회합니다. 즉, before/on/after 핸들러가 실행되지 않고, 인증 체크나 입력 검증도 건너뜁니다. PersistenceService(또는 직접 DB 서비스)는 이 우회 문제를 의도적으로 활용하거나, 반대로 피해야 할 때를 구분하는 핵심 개념입니다.

cds.connect.to('db') vs 서비스 레이어 호출 비교

두 방식의 차이를 코드로 직접 비교합니다.

// 방식 A: 서비스 레이어 (핸들러 파이프라인 통과)
const OrdersService = await cds.connect.to('OrdersService');
const result = await OrdersService.run(
  SELECT.from('Orders').where({ buyerId: 'C001' })
);

// 방식 B: DB 직접 접근 (핸들러 우회)
const db = await cds.connect.to('db');
const result = await db.run(
  SELECT.from('Orders').where({ buyerId: 'C001' })
);

방식 A는 OrdersService에 정의된 @requires, before READ 핸들러, after READ 핸들러를 모두 거칩니다. 방식 B는 DB에 직접 SELECT를 날립니다. 권한 체크나 데이터 마스킹이 있다면 방식 B에서는 작동하지 않습니다.

직접 DB 접근이 적합한 시나리오

방식 B가 올바른 선택인 경우가 있습니다.

// 시나리오: 백그라운드 잡에서 만료된 세션 정리
// 서비스 핸들러의 인증 체크를 거칠 필요 없음
const db = await cds.connect.to('db');

// 7일 이전 세션 삭제
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const deleted = await db.run(
  DELETE.from('UserSessions').where({ lastAccessedAt: { '<=': cutoff } })
);
console.log(`Cleaned up ${deleted} expired sessions`);

이 경우 UserSessions를 공개하는 서비스 레이어를 거치면 불필요한 인증/권한 처리가 추가됩니다. 내부 배치 작업에서는 db에 직접 접근하는 것이 더 명확합니다.

cds.ql로 타입 안전 쿼리 구성

CAP의 CQL(CDS Query Language)은 문자열이 아닌 객체 형태로 쿼리를 구성할 수 있습니다.

const { SELECT, INSERT, UPDATE, DELETE } = cds.ql;

// SELECT with projection and filter
const invoices = await db.run(
  SELECT.from('Invoices', ['invoiceId', 'amount', 'dueDate'])
        .where({ status: 'PENDING', amount: { '>': 1000 } })
        .orderBy('dueDate')
        .limit(50)
);

// INSERT
await db.run(
  INSERT.into('Invoices').entries({
    invoiceId: 'INV-2026-001',
    amount: 5000,
    status: 'PENDING',
    dueDate: '2026-06-30'
  })
);

// UPDATE
await db.run(
  UPDATE('Invoices')
    .set({ status: 'PAID', paidAt: new Date() })
    .where({ invoiceId: 'INV-2026-001' })
);

이 방식은 SQL 문자열 인젝션 위험이 없고, IDE 자동완성을 활용할 수 있습니다.

트랜잭션 처리: cds.tx()로 원자성 보장

여러 DML을 하나의 트랜잭션으로 묶으려면 cds.tx()를 사용합니다.

// 주문 생성 + 재고 차감을 원자적으로 처리
await cds.tx(async (tx) => {
  // 1. 주문 헤더 INSERT
  await tx.run(
    INSERT.into('SalesOrders').entries({
      orderId: 'SO-2026-789',
      customerId: 'CUST-042',
      totalAmount: 12500,
      status: 'CONFIRMED'
    })
  );

  // 2. 재고 차감
  await tx.run(
    UPDATE('ProductInventory')
      .set({ qty: { '-=': 3 } })
      .where({ productId: 'PROD-A1', qty: { '>=': 3 } })
  );

  // 3. 재고 부족 체크
  const inv = await tx.run(
    SELECT.one.from('ProductInventory').where({ productId: 'PROD-A1' })
  );
  if (!inv || inv.qty < 0) {
    throw new Error('재고 부족 — 트랜잭션 롤백');
  }
});

cds.tx() 내부에서 예외가 발생하면 자동으로 롤백됩니다. 정상 종료되면 커밋됩니다.

외부 서비스와 DB를 함께 쓸 때 주의점

CAP에서 외부 OData 서비스와 로컬 DB를 혼합할 때 트랜잭션 경계를 명확히 해야 합니다.

this.on('CREATE', 'PurchaseOrders', async (req) => {
  const db = await cds.connect.to('db');
  const ExternalSAP = await cds.connect.to('ExternalS4');

  // 1. 외부 시스템에 먼저 전송 (실패하면 로컬 저장 안 함)
  const extResult = await ExternalSAP.run(
    INSERT.into('A_PurchaseOrder').entries(req.data)
  );

  // 2. 외부 성공 후 로컬 DB에 기록
  await db.run(
    INSERT.into('LocalPurchaseOrders').entries({
      ...req.data,
      externalId: extResult.PurchaseOrder,
      syncedAt: new Date()
    })
  );

  return extResult;
});

외부 서비스 호출은 로컬 트랜잭션에 포함되지 않습니다. 외부 성공 후 로컬 INSERT가 실패하는 경우를 대비해 보상 트랜잭션(compensating transaction) 또는 재시도 로직이 필요합니다.

CAP Java에서의 PersistenceService

Java CAP에서는 PersistenceService 인터페이스를 주입해서 사용합니다.

@Autowired
PersistenceService persistenceService;

public void archiveOldRecords() {
    // 이벤트 핸들러 없이 직접 DB 접근
    CqnSelect query = Select.from(Invoices_.class)
        .where(i -> i.dueDate().lt(LocalDate.now().minusDays(90)));

    List<Invoices> old = persistenceService.run(query).listOf(Invoices.class);

    // 아카이브 처리
    old.forEach(inv -> {
        // ...처리 로직
    });
}

Node.js의 cds.connect.to('db')와 동일한 역할입니다. CDS 이벤트 파이프라인을 우회해 순수 DB 작업을 실행합니다.

언제 어떤 방식을 선택해야 하는가

  • 서비스 핸들러 파이프라인이 필요한 경우(인증, 검증, 가공): 서비스 레이어 호출
  • 내부 배치 작업, 마이그레이션, 시스템 레벨 처리: 직접 DB 접근
  • 여러 엔티티를 원자적으로 변경: cds.tx() 사용
  • 외부 서비스와 로컬 DB 혼합: 보상 트랜잭션 패턴 고려

공식 문서

CAP 데이터베이스 접근 전체 가이드는 cap.cloud.sap/docs/node.js/databases에서 확인할 수 있습니다. CQL 문법, 트랜잭션 처리, 서비스 연결 방법이 상세히 다루어져 있습니다.

댓글 0

아직 댓글이 없습니다.