CAP for Node

Hybrid Testing 90%가 모르는 cds bind #shorts #SAP #CAP

▶ YouTube에서 보기

개요와 이 글에서 얻어갈 것

CAP(Cloud Application Programming Model) for Node.js 프로젝트를 개발할 때 대부분의 개발자는 cds watch로 로컬 SQLite와 목업(mock) 인증만 사용합니다. 문제는 배포 후에야 "BTP의 XSUAA 인증 흐름"이나 "HANA Cloud 스키마 차이"가 드러나는 경우가 많다는 것입니다. 이 글에서는 cds bind를 활용한 Hybrid Testing 방식으로, 로컬에서 실행하되 BTP에 배포된 실제 서비스(HANA Cloud, Destination, XSUAA 등)에 연결하는 3단계 전략을 다룹니다.

  • Hybrid Testing이 왜 필요한지 배경과 동작 원리 이해
  • cds bind로 Cloud Foundry 서비스 인스턴스를 로컬 프로세스에 주입하는 방법
  • .cdsrc-private.json과 profile 분리로 자격증명 안전 관리
  • PurchaseOrder 서비스 예제를 통한 3단계 실전 적용
  • 흔한 인증/네트워크 오류 트러블슈팅

사전에 갖춰둬야 할 개념

이 글은 CAP 기본 프로젝트 구조(db/, srv/, app/)와 cds watch 실행 경험이 있다고 가정합니다. 또한 Cloud Foundry CLI(cf) 기본 명령어(cf login, cf services)와 BTP에서 HANA Cloud 인스턴스 및 XSUAA 인스턴스가 이미 프로비저닝된 상태를 전제로 합니다. Node.js 18 이상, @sap/cds-dk 7.x 이상이 필요합니다.

환경과 준비물 체크리스트

버전 호환성이 Hybrid Testing에서 가장 큰 함정입니다. 실무에서 검증된 조합은 다음과 같습니다.

  • Node.js 20.x LTS (18도 가능하나 20 권장)
  • @sap/cds 7.9 이상, @sap/cds-dk 7.9 이상
  • Cloud Foundry CLI v8 (v7도 동작하나 cf8가 일반적으로 안정적)
  • BTP Trial 또는 Enterprise 계정, HANA Cloud 인스턴스(hana 플랜) 프로비저닝 완료
  • XSUAA 인스턴스(application 플랜) 프로비저닝 완료
  • @sap/xssec, @sap/xsenv, passport 의존성 설치
// package.json (일부)
"dependencies": {
  "@sap/cds": "^7.9.0",
  "@sap/xssec": "^4.2.0",
  "@sap/xsenv": "^5.2.0",
  "@sap/hana-client": "^2.20.0",
  "passport": "^0.7.0"
},
"devDependencies": {
  "@sap/cds-dk": "^7.9.0"
}

Hybrid Testing 동작 원리와 배경

CAP 런타임은 cds.env 객체에서 서비스 연결 정보를 읽습니다. 로컬에서는 기본적으로 cds.requires.db.kind = "sqlite", cds.requires.auth.kind = "mocked"가 적용됩니다. 배포 환경(Cloud Foundry)에서는 VCAP_SERVICES 환경 변수가 자동 주입되어 HANA/XSUAA로 자동 전환됩니다.

Hybrid Testing의 핵심은 "로컬 프로세스에서도 VCAP_SERVICES와 유사한 자격증명을 주입"하는 것입니다. cds bind는 다음을 수행합니다.

  1. Cloud Foundry에 배포된 서비스 인스턴스에서 Service Key를 조회 (또는 신규 생성)
  2. 해당 자격증명을 .cdsrc-private.json에 profile 단위로 저장
  3. 실행 시 --profile hybrid로 profile을 활성화하면 해당 자격증명을 cds.env에 로드

비유하자면, 배송센터(BTP Cloud)에 있는 물류 시스템에 로컬 검수 담당자가 원격 접속 케이블을 꽂아 실물 재고 데이터를 조회하는 것과 같습니다. 재고(DB)와 신분증 발급(XSUAA)은 여전히 클라우드에 있지만, 검수 로직(코드)은 로컬 노트북에서 실행됩니다.

주의할 점은 .cdsrc-private.json에는 실제 HANA 비밀번호와 XSUAA client secret이 담기므로 반드시 .gitignore에 추가해야 합니다. CAP 7.x부터는 프로젝트 초기화 시 자동으로 .gitignore에 포함되지만, 기존 프로젝트를 마이그레이션할 때는 수동 확인이 필요합니다.

실전 3단계 - PurchaseOrder 서비스 예제

구매발주(PurchaseOrder) 관리 서비스를 예로, 로컬 SQLite에서 시작해 BTP HANA와 XSUAA에 순차적으로 바인딩하는 과정을 다룹니다.

먼저 도메인 모델과 서비스 정의입니다.

// db/schema.cds
namespace procurement;
using { managed, cuid } from '@sap/cds/common';

entity PurchaseOrders : cuid, managed {
  orderNo      : String(20);
  supplier     : String(100);
  totalAmount  : Decimal(15,2);
  currency     : String(3);
  status       : String(20) default 'DRAFT';
  items        : Composition of many PurchaseOrderItems on items.parent = $self;
}

entity PurchaseOrderItems : cuid {
  parent       : Association to PurchaseOrders;
  material     : String(40);
  quantity     : Integer;
  unitPrice    : Decimal(15,2);
}
// srv/procurement-service.cds
using { procurement as db } from '../db/schema';

@requires: 'authenticated-user'
service ProcurementService @(path: '/procurement') {
  entity PurchaseOrders as projection on db.PurchaseOrders;
  entity PurchaseOrderItems as projection on db.PurchaseOrderItems;

  action approve(orderID: UUID) returns PurchaseOrders;
}

1단계 - 로컬 기본 실행으로 베이스라인 확보

가장 먼저 로컬에서 SQLite와 mocked 인증으로 동작을 확인합니다. 이 단계 없이 바로 hybrid로 넘어가면 나중에 오류 원인이 코드인지 바인딩인지 구분이 안 됩니다.

// srv/procurement-service.js
const cds = require('@sap/cds');

module.exports = class ProcurementService extends cds.ApplicationService {
  init() {
    const { PurchaseOrders } = this.entities;

    this.before('CREATE', PurchaseOrders, (req) => {
      if (!req.data.orderNo) {
        req.data.orderNo = 'PO-' + Date.now();
      }
    });

    this.on('approve', async (req) => {
      const { orderID } = req.data;
      const order = await SELECT.one.from(PurchaseOrders).where({ ID: orderID });
      if (!order) req.reject(404, `Order ${orderID} not found`);
      if (order.status !== 'DRAFT') {
        req.reject(400, `Only DRAFT orders can be approved (current: ${order.status})`);
      }
      await UPDATE(PurchaseOrders).set({ status: 'APPROVED' }).where({ ID: orderID });
      return { ...order, status: 'APPROVED' };
    });

    return super.init();
  }
};
# 실행
cds watch
# 브라우저에서 http://localhost:4004 접속 후 mocked 사용자로 테스트

2단계 - HANA Cloud 바인딩과 로그 강화

이제 로컬 프로세스가 BTP HANA Cloud를 DB로 사용하도록 바인딩합니다. Cloud Foundry에 로그인 후 대상 space를 지정합니다.

cf login -a https://api.cf.eu10.hana.ondemand.com
cf target -o my-org -s dev

# HANA 서비스 인스턴스 이름 확인
cf services

# cds bind - 자동으로 service key 생성 및 저장
cds bind -2 procurement-hana:procurement-hana-key --kind hana

실행 후 .cdsrc-private.json이 생성됩니다.

{
  "requires": {
    "[hybrid]": {
      "db": {
        "kind": "hana",
        "binding": {
          "type": "cf",
          "apiEndpoint": "https://api.cf.eu10.hana.ondemand.com",
          "org": "my-org",
          "space": "dev",
          "instance": "procurement-hana",
          "key": "procurement-hana-key",
          "resolved": false
        },
        "vcap": { "label": "hana" }
      }
    }
  }
}

실행 시 profile을 활성화합니다.

# DB만 hybrid로 (인증은 여전히 mocked)
cds watch --profile hybrid

# 스키마 자동 배포
cds deploy --profile hybrid --to hana

실무에서는 커스텀 로거로 어떤 서비스가 바인딩됐는지 확인하는 습관이 중요합니다.

// srv/server.js
const cds = require('@sap/cds');

cds.on('bootstrap', () => {
  const log = cds.log('bindings');
  const db = cds.env.requires.db;
  const auth = cds.env.requires.auth;
  log.info('DB kind:', db?.kind, '| credentials host:', db?.credentials?.host || 'local');
  log.info('Auth kind:', auth?.kind);
});

3단계 - XSUAA와 Destination까지 프로덕션급 바인딩

실제 사용자 JWT 토큰으로 인증 흐름까지 검증하려면 XSUAA도 바인딩합니다. 여기에 외부 시스템 연동을 위한 Destination 서비스까지 묶으면 프로덕션과 거의 동일한 환경이 됩니다.

cds bind -2 procurement-xsuaa:procurement-xsuaa-key --kind xsuaa
cds bind -2 procurement-dest:procurement-dest-key --kind destination

# 여러 바인딩 한번에 지정
cds bind --list

실행할 때는 다음과 같이 프로세스를 띄웁니다.

cds watch --profile hybrid

인증 테스트를 위해 XSUAA에서 발급받은 JWT를 사용해야 합니다. Business Application Studio 또는 로컬에서 다음처럼 토큰을 획득합니다.

# XSUAA credentials에서 url, clientid, clientsecret 확인
cf service-key procurement-xsuaa procurement-xsuaa-key

# Password grant로 토큰 획득
curl -X POST "$XSUAA_URL/oauth/token" \
  -u "$CLIENTID:$CLIENTSECRET" \
  -d "grant_type=password&username=$USER&password=$PASS"

발급받은 토큰으로 로컬 CAP 서버를 호출합니다.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:4004/procurement/PurchaseOrders

테스트 자동화를 위한 Jest 설정 예시입니다.

// test/procurement.test.js
const cds = require('@sap/cds/lib');
const { GET, POST, expect } = cds.test(__dirname + '/..').in(__dirname + '/..');

// hybrid profile 활성화
cds.env.profiles = ['hybrid'];

describe('ProcurementService (hybrid)', () => {
  it('creates order with auto-generated orderNo', async () => {
    const { status, data } = await POST('/procurement/PurchaseOrders', {
      supplier: 'ACME Corp',
      totalAmount: 15000.00,
      currency: 'EUR'
    }, {
      auth: { bearer: process.env.TEST_JWT }
    });
    expect(status).to.equal(201);
    expect(data.orderNo).to.match(/^PO-/);
  });
});

흔한 실수와 트러블슈팅 FAQ

Q1. cds bind 실행 시 "No service key found"가 나옵니다.
Service Key가 아직 생성되지 않은 상태입니다. cf create-service-key procurement-hana procurement-hana-key로 먼저 생성하거나, cds bind--create-key 옵션을 추가하면 자동 생성됩니다.

Q2. 로컬에서 HANA 연결이 "connection refused" 또는 timeout으로 실패합니다.
BTP HANA Cloud는 IP 화이트리스트가 기본 활성화되어 있습니다. BTP Cockpit의 HANA Cloud Central에서 개발자의 공인 IP를 Allowed Connections에 추가해야 합니다. VPN 사용 시 VPN IP를 등록해야 하고, 재택근무자의 유동 IP는 0.0.0.0/0을 임시 사용하되 프로덕션에서는 절대 사용하지 않는 것이 권장됩니다.

Q3. XSUAA 바인딩 후 401 Unauthorized만 반환됩니다.
xs-security.json의 scope와 role-template이 XSUAA 인스턴스에 반영되지 않은 경우가 대부분입니다. cf update-service procurement-xsuaa -c xs-security.json으로 갱신 후, XSUAA는 재바인딩(cds bind)이 필요합니다. 또한 password grant flow가 XSUAA 인스턴스에서 허용되어 있는지 확인해야 합니다.

Q4. profile을 활성화했는데 여전히 SQLite에 연결됩니다.
cds watch --profile hybrid 대신 cds watch hybrid로 잘못 입력한 경우가 흔합니다. 또한 환경변수 CDS_ENV=hybrid를 명시하는 방법도 있습니다. cds env requires.db --profile hybrid로 실제 로드된 설정을 확인해 보세요.

Q5. .cdsrc-private.json이 실수로 커밋됐습니다.
즉시 cf delete-service-key로 해당 Service Key를 삭제하고 재발급받아야 합니다. Git 히스토리에서도 git filter-repo 등으로 완전히 제거하는 것이 안전합니다.

이후 확장 가능한 주제들

Hybrid Testing에 익숙해지면 다음 주제로 넘어가는 것이 자연스럽습니다. 첫째, cds-plugin으로 커스텀 바인딩 로직 확장. 둘째, GitHub Actions에서 cf service-key를 secret으로 주입해 CI에서 hybrid 통합 테스트 실행. 셋째, Multitenant SaaS 시나리오에서 tenant별 HANA 컨테이너 바인딩. 넷째, @sap/cds-mtxs를 활용한 subscribe/unsubscribe 흐름의 로컬 재현. 다섯째, Approuter를 로컬에서 함께 띄워 UI5 앱까지 end-to-end 검증하는 방식입니다.

더 깊이 있는 자료

댓글 0

아직 댓글이 없습니다.