CAP for Node

CAP Node.js 실수 패턴 — 30초 만에 정리 #shorts #SAP #CAPforNode

▶ YouTube에서 보기

왜 CAP Node.js 프로젝트에서 같은 실수가 반복되는가

SAP Cloud Application Programming Model(CAP) for Node.js는 convention over configuration 철학을 기반으로 하기 때문에, 초기 진입 장벽은 낮지만 프로덕션에 근접할수록 프레임워크 내부 동작을 이해하지 못한 개발자가 반복해서 부딪히는 함정이 존재합니다. 특히 CDS 모델링, 이벤트 핸들러 흐름, HANA/SQLite 어댑터의 차이, 그리고 SAP BTP Cloud Foundry 환경의 자격증명 바인딩 방식은 문서가 광범위하게 흩어져 있어 "동작은 하지만 잘못된 코드"가 배포되는 경우가 흔합니다.

이 글에서는 실제 SAP BTP 프로젝트(구독 관리, 구매요청, 협력사 포털 등)에서 반복적으로 관찰되는 5가지 실수 패턴을 CAP Node.js 8.x(@sap/cds 8.x) 기준으로 정리하고, 각 실수의 근본 원인과 재현 가능한 수정 방향을 제시합니다.

  • 이벤트 루프 블로킹 회피 전략 이해
  • CDS 쿼리 API와 raw SQL의 경계 판단
  • 핸들러 실행 순서(before/on/after) 정확히 파악
  • 멀티테넌시(mtxs) 및 자격증명 관리 원칙 습득

이 글을 이해하기 위해 필요한 배경

Node.js의 이벤트 루프와 async/await 개념, CAP의 cds.Service 구조, CDS 엔티티/뷰 정의, 그리고 SAP BTP Cloud Foundry의 서비스 바인딩(VCAP_SERVICES) 개념에 대한 기본적인 이해가 필요합니다. CAP 프로젝트를 최소 한 번은 cds watch로 실행하고 SQLite 기반으로 배포 경험이 있는 중급 개발자 수준을 가정합니다. HANA Cloud, XSUAA, Approuter 같은 SaaS 구성 요소는 등장하되, 각 상세 설정은 하이레벨로만 다룹니다.

실습 환경 및 준비 사항

이 글의 예제는 다음 환경에서 검증되었습니다.

  • Node.js 20 LTS (npm 10+)
  • @sap/cds 8.2.x, @sap/cds-dk 8.2.x
  • @cap-js/hana 1.x, @cap-js/sqlite 1.x
  • @sap/xssec 4.x, @sap/xsenv 5.x
  • SAP BTP Cloud Foundry (Trial 또는 Enterprise), HANA Cloud instance
  • 선택: @sap/cds-mtxs 1.x (멀티테넌시)

프로젝트 초기화는 cds init po-service --add sample 명령으로 시작할 수 있으며, 데모 도메인은 "구매요청(PurchaseOrder)" 시나리오로 통일합니다. 로컬 개발은 SQLite로, 프로덕션은 HANA Cloud로 이원화한 상태를 가정합니다.

핵심 개념 정리 — CAP 이벤트 파이프라인의 구조

CAP Node.js는 요청이 들어오면 다음과 같은 파이프라인을 통과합니다. 이 흐름을 한 장의 다이어그램으로 표현하면 "요청 → auth → before(N개) → on(1개) → after(N개) → 응답"의 파이프 구조입니다.

비유하자면 공장의 컨베이어 벨트입니다. before는 원재료 검수(검증/정규화), on은 실제 조립(비즈니스 로직 + DB), after는 포장(응답 가공)입니다. before/after는 여러 개가 순서대로 실행되지만, on은 단 하나만 실행되며 next()를 호출해야 다음 핸들러로 넘어갑니다.

또한 CAP은 CQN(CDS Query Notation)이라는 중간 언어를 사용합니다. SELECT.from(PurchaseOrders) 같은 표현은 파서 없이 곧바로 SQL로 변환될 수 있는 트리 구조이며, HANA/SQLite/PostgreSQL 각 어댑터가 이 CQN을 자기 방언(dialect)의 SQL로 컴파일합니다. 이 추상화 덕분에 DB 이식성이 확보되지만, CQN이 표현할 수 없는 쿼리(윈도우 함수, 특정 힌트 등)는 raw SQL로 우회해야 합니다.

여기에 tenant-aware 구조가 얹혀 있습니다. cds.context.tenant 값에 따라 자동으로 스키마가 분리되며, mtxs(멀티테넌시 확장)를 사용하면 구독 시점에 HDI 컨테이너가 자동 프로비저닝됩니다. 이 세 축(파이프라인, CQN, 테넌시)을 머릿속에 담고 나면 뒤이어 나오는 5가지 실수의 원인이 자연스럽게 보입니다.

실수 1 — 이벤트 루프 블로킹: 동기 처리와 큰 배열 순회

가장 흔하게 목격되는 실수는 핸들러 내부에서 동기 파일 I/O나 큰 배열의 순차 await를 남용하는 것입니다. 아래는 구매요청 첨부파일 유효성을 검증하는 잘못된 예제입니다.

// srv/purchase-order-service.js  (안티패턴)
const fs = require('fs');
const cds = require('@sap/cds');

module.exports = cds.service.impl(async function () {
  this.before('CREATE', 'PurchaseOrders', async (req) => {
    // 안티패턴 1: 동기 파일 I/O — 이벤트 루프 정지
    const schema = fs.readFileSync('./schemas/po-schema.json', 'utf8');

    // 안티패턴 2: N개 항목을 순차 await
    for (const item of req.data.Items) {
      await validateAgainstSchema(item, schema); // 100건이면 100번 순차 대기
    }
  });
});

수정 방향은 두 가지입니다. 첫째, 스키마 같은 정적 자원은 부팅 시점에 한 번만 비동기로 로드하여 캐시합니다. 둘째, 순차 await 대신 Promise.all로 병렬화하되, 외부 API 호출이 섞여 있다면 p-limit으로 동시성을 제한합니다.

const fs = require('fs/promises');
const pLimit = require('p-limit');
const cds = require('@sap/cds');

let schemaCache;
async function getSchema () {
  if (!schemaCache) schemaCache = JSON.parse(await fs.readFile('./schemas/po-schema.json', 'utf8'));
  return schemaCache;
}

module.exports = cds.service.impl(async function () {
  const limit = pLimit(8);
  this.before('CREATE', 'PurchaseOrders', async (req) => {
    const schema = await getSchema();
    await Promise.all(req.data.Items.map(item => limit(() => validateAgainstSchema(item, schema))));
  });
});

실수 2 — CDS 쿼리 남용 vs 적절한 raw SQL

반대 방향의 실수도 있습니다. 모든 상황에서 raw SQL로 우회하거나, 반대로 복잡한 집계까지 CQN으로 억지로 표현하는 경우입니다. CAP은 CQN을 우선 권장하지만, 복잡한 리포팅 쿼리는 HANA의 계산뷰(Calculation View) 또는 cds.db.run()으로 우회하는 편이 유지보수 관점에서 낫습니다.

다음은 부서별 미결 구매요청 금액 상위 10건을 조회하는 실전 예제입니다. 1단계는 CQN으로만 시도한 안티패턴, 2단계는 raw SQL로 명확하게 분리한 개선안입니다.

// 안티패턴: 복잡한 집계를 CQN으로 억지 구성
const rows = await SELECT.from('PurchaseOrders')
  .columns('department', { func: 'SUM', args: [{ ref: ['amount'] }], as: 'total' })
  .where({ status: 'OPEN' })
  .groupBy('department')
  .orderBy({ ref: ['total'], sort: 'desc' })
  .limit(10);
// 개선안: HANA 방언 활용, 파라미터 바인딩
const sql = `
  SELECT department, SUM(amount) AS total
  FROM PO_PURCHASEORDERS
  WHERE status = ?
  GROUP BY department
  ORDER BY total DESC
  LIMIT 10
`;
const rows = await cds.db.run(sql, ['OPEN']);

단, raw SQL은 CDS 접근제어(@restrict)와 tenant 분리 로직을 자동으로 적용하지 않으므로, 반드시 cds.context.tenant를 명시적으로 반영하거나 tx 컨텍스트(cds.tx(req).run(...))를 통해 실행해야 합니다.

실수 3 — before / on / after 실행 순서 오해

많은 개발자가 this.on('CREATE', ...)을 등록하면 기본 CRUD가 실행되지 않는다는 사실을 모르고, 검증 로직을 on에 넣었다가 데이터가 저장되지 않아 당황합니다.

// 안티패턴: on을 등록해 기본 저장 로직을 덮어씀
this.on('CREATE', 'PurchaseOrders', async (req) => {
  if (req.data.amount < 0) req.reject(400, '금액은 0 이상이어야 합니다');
  // 반환값이 undefined → CAP은 저장을 시도하지 않음
});

올바른 방식은 검증을 before에 두거나, on에서 next()를 호출해 기본 처리에 위임하는 것입니다.

this.before('CREATE', 'PurchaseOrders', (req) => {
  if (req.data.amount < 0) req.reject(400, '금액은 0 이상이어야 합니다');
});

// 필요 시 on에서는 next()로 기본 처리 위임
this.on('CREATE', 'PurchaseOrders', async (req, next) => {
  const result = await next();
  await auditLog.record('PO_CREATED', result.ID);
  return result;
});

this.after('CREATE', 'PurchaseOrders', (data, req) => {
  req.info(`PO ${data.ID} 생성 완료`);
});

실수 4 — 멀티테넌트(mtxs) 설정 없이 SaaS 배포

단일 테넌트로 개발하다가 그대로 SaaS로 배포하는 경우, 모든 구독자가 동일한 HDI 컨테이너를 공유하여 데이터가 섞이는 심각한 사고가 발생할 수 있습니다. CAP은 @sap/cds-mtxs 패키지로 구독 시 자동 프로비저닝을 지원합니다.

{
  "dependencies": {
    "@sap/cds": "^8",
    "@sap/cds-mtxs": "^1",
    "@cap-js/hana": "^1"
  },
  "cds": {
    "requires": {
      "multitenancy": true,
      "db": { "kind": "hana-cloud" },
      "auth": { "kind": "xsuaa" }
    }
  }
}

배포 시에는 mtx-sidecar를 별도 애플리케이션으로 분리하거나, 인-프로세스 모드로 통합할 수 있습니다. 구독 콜백(onSubscribe) 훅에서 라우팅 정보를 App Router에 등록하는 것을 잊지 않도록 주의해야 합니다.

실수 5 — 자격증명 하드코딩과 VCAP_SERVICES 무시

가장 위험한 실수는 HANA 접속 정보나 XSUAA client secret을 소스에 직접 넣는 것입니다. Cloud Foundry에서는 서비스 바인딩이 VCAP_SERVICES라는 환경변수로 주입되므로, 이를 @sap/xsenv로 읽는 것이 표준입니다.

// 안티패턴
const conn = hanaClient.createConnection();
conn.connect({
  serverNode: 'my-hana.eu10.hanacloud.ondemand.com:443',
  uid: 'DBADMIN',
  pwd: 'SuperSecret123!'   // 소스코드에 시크릿 노출
});
// 개선안 — CAP이 VCAP_SERVICES를 자동으로 읽어 연결
const db = await cds.connect.to('db');

// 직접 자격증명이 필요한 경우
const xsenv = require('@sap/xsenv');
xsenv.loadEnv();   // 로컬: default-env.json 로드
const { credentials } = xsenv.getServices({ hana: { tag: 'hana' } }).hana;

실수 패턴의 공통 원인과 예방 원칙

다섯 가지 실수를 관통하는 공통 원인은 세 가지로 요약됩니다.

  1. 추상화의 경계 오해 — CAP은 많은 것을 자동으로 처리하지만 무엇을 하지 않는지도 명확히 알아야 합니다.
  2. 로컬 vs 클라우드 환경 갭 — SQLite/기본 auth로만 검증하고 HANA/XSUAA 문제를 배포 후에 발견합니다.
  3. 파이프라인 흐름의 시각화 부재 — before/on/after를 다이어그램 없이 상상으로만 다룹니다.

예방 전략으로는 (a) hybrid 모드(cds bind)로 로컬에서 실제 클라우드 서비스에 연결해 테스트, (b) cds.log를 활용한 이벤트 훅별 로그 표준화, (c) @sap/cds/test를 사용한 인테그레이션 테스트 스위트 도입, (d) 정적 분석 도구(ESLint + eslint-plugin-security)와 npm audit의 CI 통합을 권장합니다.

흔한 질문 정리

Q1. before에서 req.data를 수정해도 될까요? 일반적으로 안전합니다. before 단계에서 정규화(normalization)를 수행하는 것이 권장 패턴입니다. 다만 참조 무결성이 관련된 필드는 데이터베이스 제약과 겹치지 않도록 주의합니다.

Q2. cds.db.run(sql)SELECT.from(...) 중 무엇이 빠른가요? 컴파일 오버헤드는 CQN 쪽이 미세하게 크지만, 대부분의 워크로드에서 유의미한 차이는 없습니다. 성능보다는 유지보수성과 tenant/권한 자동 적용 여부로 선택하는 편이 좋습니다.

Q3. 멀티테넌시를 나중에 붙일 수 있나요? 가능은 하지만 데이터 마이그레이션 비용이 큽니다. SaaS 가능성이 조금이라도 있다면 초기부터 @sap/cds-mtxs를 도입하는 것이 안전합니다.

이후 확장 방향과 결론

이 글의 다섯 가지 실수를 확실히 이해했다면 다음 주제로 확장하기를 권장합니다. (1) CAP의 Outbox 패턴과 메시징(Event Mesh) 연동, (2) Fiori Elements 어노테이션을 활용한 UI 최적화, (3) HANA Cloud의 Native SQL 뷰와 CDS 뷰의 조합, (4) BTP Alert Notificationcds.log를 이용한 프로덕션 모니터링, (5) @sap/cds/test와 Jest 결합을 통한 회귀 테스트 자동화.

더 깊이 들어갈 수 있는 자료

  • CAP Node.js Reference — 이벤트 핸들러 파이프라인 공식 문서
  • CAP Multitenancy Guide — mtxs 설정 및 구독 콜백
  • CAP Core Services — Event Handlers before/on/after 실행 순서
  • SAP BTP — Development of Multitenant Applications
  • SAP HANA Cloud 공식 문서 — HDI 컨테이너 프로비저닝
  • Node.js 공식 문서 — Don't Block the Event Loop

댓글 0

아직 댓글이 없습니다.