이 예제에서 다루는 내용과 도달 지점
CAP(Cloud Application Programming Model) 기반 Node.js 프로젝트를 만들다 보면 대부분 cds watch로 로컬 인메모리 SQLite에서만 개발을 진행합니다. 그러다 BTP(Business Technology Platform)에 배포하는 순간 SAP HANA Cloud 연결 오류, Destination 인증 실패, XSUAA 스코프 문제 등 로컬에서는 보이지 않던 이슈가 한꺼번에 터집니다. 이 글에서는 Hybrid Testing이라는 접근법을 통해 로컬에서 실행 중인 Node.js 프로세스가 실제 BTP에 바인딩된 서비스(HANA, Destination, XSUAA 등)를 직접 호출하도록 3단계로 구성해 보겠습니다.
- 로컬 CAP 앱을 실제 BTP 서비스 인스턴스와 바인딩하는 방법 이해
cds bind와cds watch --profile hybrid의 동작 원리 파악- HANA Cloud 스키마와 로컬 SQLite 사이의 전환 전략 수립
- 실무 파이프라인에서 CI/CD 이전에 조기 통합 결함을 잡아내는 실전 패턴 적용
미리 알아두면 좋은 배경 지식
이 글은 CAP 프로젝트를 cds init으로 한 번이라도 만들어 본 개발자를 대상으로 합니다. @sap/cds 8.x 계열 CLI 사용법, package.json의 cds.requires 블록 구성, Cloud Foundry(cf) 또는 Kyma에 이미 서비스 인스턴스(HANA HDI Container, Destination Service 등)를 한 개 이상 생성해 본 경험이 필요합니다. Node.js 20 LTS 기반 환경, mta.yaml 구성에 대한 기초 지식이 있으면 이해가 수월합니다.
구성 환경과 필요한 도구
이 글에서 사용하는 버전 조합은 다음과 같습니다. 조직 환경에 따라 minor 버전은 다를 수 있으니 최신 릴리스 노트를 확인하기를 권장합니다.
@sap/cds-dk8.2 이상 (권장 8.4 LTS 라인)- Node.js 20.11 LTS
- Cloud Foundry CLI 8.x 및
cf login완료 상태 - BTP 서브계정에 HANA Cloud HDI Container 인스턴스 1개 (플랜:
hdi-shared) - 선택: Destination Service 인스턴스 (플랜:
lite), XSUAA 인스턴스 (플랜:application) - OS: macOS 14 / Windows 11 / Linux (WSL2 포함) 모두 검증 가능
설치 확인은 다음 한 줄로 충분합니다.
$ cds --version
@sap/cds-dk: 8.4.1
@sap/cds: 8.4.2
Node.js: v20.11.1
Hybrid Testing의 동작 원리와 핵심 개념
CAP은 런타임 시 cds.env가 로드한 프로파일에 따라 서비스 바인딩을 다르게 해석합니다. 로컬에서는 [development] 프로파일이 기본으로 활성화되어 db는 SQLite, auth는 mocked로 잡힙니다. 반대로 Cloud Foundry에 배포되면 [production] 프로파일이 활성화되고 VCAP_SERVICES 환경변수에서 실제 서비스 자격증명을 읽습니다.
Hybrid는 이 두 세계 사이의 회색지대입니다. 로컬 프로세스로 실행되지만 자격증명은 클라우드에서 받아오는 구조로, 마치 "노트북에 원격 데이터베이스 콘솔을 꽂아둔" 형태에 비유할 수 있습니다.
비유: 로컬 개발은 나만의 방에서 스케치, Production은 완성된 갤러리 전시, Hybrid는 갤러리의 조명 아래서 스케치하는 것과 같습니다. 조명(HANA)의 실제 색감을 확인하며 그림(코드)을 그릴 수 있죠.
기술적으로는 cds bind 명령이 .cdsrc-private.json 파일에 서비스 인스턴스의 GUID를 기록하고, cds watch --profile hybrid 실행 시 CAP 런타임이 Cloud Foundry API를 호출해 service key를 임시로 생성/조회한 뒤 그 자격증명으로 원격 서비스에 접속합니다. 이 자격증명은 프로세스 메모리에만 유지되어 소스 리포지토리에 커밋되지 않는 것이 핵심 안전장치입니다.
또한 CAP는 cds.requires.db.kind가 hana로 잡히면 자동으로 @sap/hana-client 드라이버를 활성화하고, SQLite 방언과 HANA 방언 사이의 SQL 차이를 컴파일 타임에 해석해 줍니다. 따라서 개발자는 CDS 모델만 유지하면 되고, 실제 실행 대상 DB에 따라 CQN(CDS Query Notation)이 다른 SQL로 변환됩니다.
Hybrid Testing이 특히 유용한 시나리오는 다음과 같습니다.
- HANA 전용 함수(예:
GENERATE_SERIES, 지리공간ST_*)를 사용하는 계산 뷰 - Destination을 통한 S/4HANA OData 콜(로컬 mocking으로는 재현 어려운 응답 헤더/CSRF)
- XSUAA JWT 토큰 검증 및 스코프 기반 인가 로직
1단계 — 로컬 프로젝트에 서비스 인스턴스 바인딩
주문 관리(SalesOrder) 도메인의 CAP 프로젝트를 예로 들겠습니다. 먼저 프로젝트를 초기화합니다.
$ cds init order-mgmt --add hana,mta
$ cd order-mgmt
$ npm install
db/schema.cds에 최소한의 엔티티를 정의합니다.
namespace com.acme.order;
using { managed, cuid } from '@sap/cds/common';
entity SalesOrders : cuid, managed {
orderNo : String(20);
customer : String(80);
totalAmount: Decimal(15,2);
currency : String(3) default 'KRW';
items : Composition of many SalesOrderItems on items.parent = $self;
}
entity SalesOrderItems : cuid {
parent : Association to SalesOrders;
product : String(40);
qty : Integer;
price : Decimal(15,2);
}
이제 BTP에 이미 만들어 둔 HANA HDI Container를 로컬 프로젝트에 바인딩합니다. cf services로 인스턴스 이름을 확인한 뒤 다음 명령을 실행합니다.
$ cf login -a https://api.cf.ap12.hana.ondemand.com
$ cf target -o my-org -s dev
$ cds bind db --to order-mgmt-db --kind hana
실행이 끝나면 프로젝트 루트에 .cdsrc-private.json이 생성됩니다. 이 파일은 .gitignore에 자동으로 추가되며, 다음과 같이 인스턴스 참조만 포함합니다(자격증명 자체는 저장되지 않음).
{
"requires": {
"[hybrid]": {
"db": {
"kind": "hana",
"binding": {
"type": "cf",
"apiEndpoint": "https://api.cf.ap12.hana.ondemand.com",
"org": "my-org",
"space": "dev",
"instance": "order-mgmt-db",
"key": "order-mgmt-db-key"
}
}
}
}
}
이제 다음 명령으로 실제 HANA에 스키마를 배포하고 서비스를 기동합니다.
$ cds deploy --profile hybrid --to hana
$ cds watch --profile hybrid
브라우저에서 http://localhost:4004를 열면 로컬 UI지만 백엔드는 클라우드 HANA에 붙어 있는 상태가 됩니다.
2단계 — Destination 서비스 연동과 로깅 강화
실무에서는 SalesOrder를 저장하기 전 외부 S/4HANA 시스템에서 고객 마스터를 조회해야 하는 경우가 많습니다. 이때 cds-plugin-destinations를 통해 Destination을 하이브리드로 연결하고, 커스텀 서비스 핸들러에 에러/로깅을 추가해 보겠습니다.
$ cds bind dest --to my-dest-svc --kind destination
$ cds add connectivity --external S4/API_BUSINESS_PARTNER
srv/order-service.js에 다음과 같은 핸들러를 작성합니다.
const cds = require('@sap/cds');
const LOG = cds.log('order-svc');
module.exports = cds.service.impl(async function () {
const { SalesOrders } = this.entities;
const s4bp = await cds.connect.to('API_BUSINESS_PARTNER');
this.before('CREATE', SalesOrders, async (req) => {
const { customer } = req.data;
if (!customer) return req.reject(400, 'customer is required');
try {
LOG.info('Verifying customer via S/4:', customer);
const bp = await s4bp.run(
SELECT.one.from('A_BusinessPartner')
.where({ BusinessPartner: customer })
);
if (!bp) req.reject(422, `Unknown customer ${customer}`);
req.data.customerName = bp.BusinessPartnerName;
} catch (e) {
LOG.error('Destination call failed', { code: e.code, msg: e.message });
req.reject(502, 'Downstream S/4 unavailable');
}
});
});
로컬에서 cds watch --profile hybrid로 실행하면 실제 Destination이 가리키는 S/4 시스템(개발계)이 호출됩니다. 로그는 DEBUG=order-svc,cds cds watch --profile hybrid로 실행하면 더 상세히 볼 수 있습니다.
3단계 — 프로덕션 준비: 프로파일 분리, 인가, 테스트 자동화
실전에서는 hybrid를 개발자 개인의 검증용으로만 쓰고, 배포 파이프라인에서는 production 프로파일로 자동 전환되도록 package.json을 정리해야 합니다.
{
"cds": {
"requires": {
"db": { "kind": "sql" },
"auth": { "kind": "mocked" },
"[hybrid]": {
"db": { "kind": "hana" },
"auth": { "kind": "xsuaa" }
},
"[production]": {
"db": { "kind": "hana" },
"auth": { "kind": "xsuaa" }
}
}
}
}
XSUAA도 바인딩합니다.
$ cds bind auth --to order-mgmt-uaa --kind xsuaa
서비스에는 스코프 기반 인가를 걸어둡니다.
service OrderService @(requires: 'authenticated-user') {
entity SalesOrders as projection on order.SalesOrders
actions {
@(requires: 'OrderApprover') action approve();
};
}
다음으로 Jest 기반의 통합 테스트를 작성해 하이브리드 프로파일 위에서 검증합니다.
const cds = require('@sap/cds');
const { GET, POST, expect } = cds.test('serve','--profile','hybrid');
describe('SalesOrders on hybrid', () => {
test('생성 및 조회 왕복', async () => {
const { status, data } = await POST('/odata/v4/order/SalesOrders', {
orderNo: 'SO-2026-0001',
customer: '1000042',
totalAmount: 125000
});
expect(status).to.equal(201);
const list = await GET('/odata/v4/order/SalesOrders?$top=1');
expect(list.data.value[0].orderNo).to.equal('SO-2026-0001');
});
});
실행은 npx jest --runInBand로 순차 실행하는 것을 권장합니다. 병렬 실행 시 동일 HDI 컨테이너에 대한 트랜잭션 충돌이 발생할 수 있습니다. CI에서는 별도 개발자용 HANA 컨테이너를 만들지 말고, 공용 QA 컨테이너를 cds bind --to qa-order-db로 붙이거나, 완전히 격리된 컨테이너를 파이프라인에서 cf create-service로 임시 생성 후 삭제하는 패턴을 사용합니다. 보안 측면에서는 서비스 키의 유효기간을 cf update-service-key로 짧게 유지하고, 개발자 이탈 시 cf delete-service-key로 즉시 회수해야 합니다.
자주 부딪히는 함정과 해결 방법
Q1. cds bind는 성공했는데 cds watch --profile hybrid에서 "Cannot find binding for db" 오류가 납니다.
A. .cdsrc-private.json은 프로파일 이름을 hybrid로 고정합니다. 실행 시 프로파일 이름이 정확히 hybrid인지, package.json의 [hybrid] 블록에 db.kind: hana가 선언되어 있는지 확인하세요. 또한 cf target이 바인딩 당시의 org/space와 동일해야 합니다.
Q2. HANA 배포는 되는데 로컬 실행 시 "insufficient privilege: Not authorized" 오류가 발생합니다.
A. HDI 컨테이너의 서비스 키에는 두 종류 사용자(DT/RT)가 있습니다. cds deploy는 DT(설계타임) 자격증명을, 앱 런타임은 RT(런타임) 자격증명을 씁니다. CAP 8.x는 자동 선택하지만, 커스텀 스크립트에서 강제로 credentials를 오버라이드하면 이 매핑이 깨질 수 있습니다.
Q3. Destination 호출 시 로컬에서는 401, 클라우드 배포 후에는 정상입니다.
A. Destination의 인증 유형이 OAuth2SAMLBearerAssertion이면 사용자 JWT가 필요합니다. 로컬 하이브리드에서는 mock user 대신 cds.User를 XSUAA 발급 토큰으로 교체해야 합니다. cds watch --profile hybrid 시 표시되는 / 랜딩 페이지의 "Authentication" 링크를 통해 로그인 후 테스트하세요.
Q4. Git에 .cdsrc-private.json이 실수로 커밋되었습니다.
A. 해당 파일에는 자격증명 자체는 없지만 인스턴스 GUID/서비스 키 이름이 노출됩니다. 즉시 cf delete-service-key로 키를 폐기하고 새 키를 발급받은 뒤 cds bind를 다시 실행하세요. 히스토리는 git filter-repo로 정리하는 것을 권장합니다.
이후 확장해 볼 만한 주제
Hybrid Testing이 손에 익었다면 다음 단계로 확장해 보세요. Kyma 런타임에서는 cds bind --resolve와 Service Bindings의 secret mount를 이용해 유사한 흐름을 구성할 수 있습니다. 또한 Approuter + XSUAA 조합을 로컬에서 재현하고 싶다면 @sap/approuter를 별도 포트로 띄우고 destinations에 localhost:4004를 등록하는 패턴이 유효합니다. 나아가 CAP의 cds.test를 GitHub Actions에서 실행할 때는 CF_USERNAME/CF_PASSWORD를 secret으로 주입하고 파이프라인 첫 단계에서 cds bind --exec로 임시 바인딩을 수행하는 방식도 널리 쓰입니다.
더 깊이 들어가기 위한 링크
댓글 0
아직 댓글이 없습니다.