CAP for Node

cds deploy 생략 금지 — HANA 연결 깨지는 이유? #shorts #SAP #CAP

개요 및 학습 체크리스트

CAP(Cloud Application Programming Model) 프로젝트를 로컬 SQLite에서 개발하다가 BTP의 HANA Cloud로 처음 배포할 때 가장 흔히 겪는 실패가 "연결은 성공했는데 테이블이 없다" 또는 "Database user is missing" 오류입니다. 원인은 대부분 하나로 수렴합니다. cds deploy --to hana를 건너뛰고 앱만 실행했기 때문입니다. 이 글에서는 왜 이런 실패가 발생하는지, HANA Cloud 스키마 생성이 어떻게 cds deploy와 HDI(HANA Deployment Infrastructure) 컨테이너에 묶여 있는지, 그리고 실무에서 절대 실패하지 않는 3단계 배포 절차를 SalesOrder / Product / Customer 도메인 예제로 따라갑니다.

  • 로컬 SQLite 프로필과 HANA Cloud 프로필의 근본적 차이 이해
  • cds deploy --to hana가 내부적으로 실행하는 작업 흐름 파악
  • HDI 컨테이너, 서비스 바인딩, default-env.json의 관계 정리
  • 실제 에러 메시지(SQL: user missing, invalid table name 등)를 원인별로 분류
  • CI/CD 환경에서 재현 가능한 배포 스크립트 작성

이 글을 읽기 전에 알고 있으면 좋은 것

CAP Node.js 또는 Java 스택의 기본 구조(db/, srv/, app/ 폴더)를 알고 있어야 합니다. cds watch로 로컬 실행 경험, BTP Cockpit에서 서브계정과 스페이스 개념, HANA Cloud 인스턴스가 이미 프로비저닝되어 있고 Allowlist에 개발 PC IP가 등록되어 있어야 합니다. Cloud Foundry CLI(cf)와 @sap/cds-dk 글로벌 설치가 되어 있다는 전제로 진행합니다.

환경, 버전, 준비물

이 글의 검증 환경은 다음과 같습니다. 버전이 다르면 옵션명이나 기본 동작이 조금씩 달라질 수 있어 명시적으로 정리합니다.

  • Node.js 20 LTS, npm 10.x
  • @sap/cds-dk 8.x (권장, 6.x 이하는 --to hana 옵션 동작 방식이 일부 다름)
  • @sap/cds 8.x, @sap/hana-client 최신
  • Cloud Foundry CLI 8.x, MTA Build Tool(mbt) 1.2 이상(선택)
  • SAP BTP: HANA Cloud 인스턴스 running 상태, hdi-shared 서비스 플랜 사용 가능
  • 로컬에서 HANA Cloud로의 아웃바운드 443 포트 개방, HANA Cloud Allowlist에 개발자 IP 등록

준비물 체크: cf login이 성공하는지, cf marketplace -e hana-cloud에서 hdi-shared 플랜이 노출되는지 미리 확인합니다. 두 가지 중 하나라도 실패하면 cds deploy는 그 이후 어떤 단계에서든 반드시 오류로 끝납니다.

핵심 개념: cds deploy가 실제로 하는 일

많은 개발자가 cds deploy --to hana를 "테이블 만드는 명령"으로만 이해하지만, 내부적으로는 훨씬 많은 오케스트레이션이 일어납니다. 이해를 돕기 위해 비유하자면 이 명령은 "이삿짐 트럭 예약 + 짐 싸기 + 새 집 열쇠 발급 + 짐 배치"를 한 번에 해주는 컨시어지에 가깝습니다.

동작을 단계로 풀어보면 다음과 같습니다.

  1. CDS 모델 컴파일: db/schema.cdssrv/*.cds를 읽어 HANA용 .hdbtable, .hdbview 아티팩트로 변환하고 db/src/gen/ 밑에 배치합니다.
  2. HDI 컨테이너 자동 생성: 프로젝트에 바인딩된 hdi-shared 서비스 인스턴스가 없으면 cf create-service hana-cloud hdi-shared <name>을 대신 호출합니다. 컨테이너는 격리된 스키마 + 전용 DB 유저(오브젝트 오너 / 애플리케이션 유저) 세트를 뜻합니다.
  3. 서비스 키 생성 및 default-env.json 기록: 로컬에서 앱이 HANA로 접속할 수 있도록 접속정보(호스트/포트/사용자/스키마)를 프로젝트 루트의 default-env.json에 저장합니다. 이 파일이 있어야 cds watch --profile hybrid가 원격 HANA에 붙습니다.
  4. HDI Deploy 실행: 실질적인 DDL 실행은 @sap/hdi-deploy가 담당합니다. 생성된 .hdbtable을 HANA에 반영하고, 변경분만 diff 배포합니다.

여기서 결정적 포인트는, 앱을 아무리 cf push해도 4번 HDI Deploy가 실행되지 않으면 테이블은 존재하지 않는다는 점입니다. CAP 앱 컨테이너와 DB Deployer 컨테이너는 MTA에서도 완전히 분리된 모듈입니다. 로컬 개발 중에는 cds deploy --to hana가 이 4단계를 모두 대행하지만, 프로덕션에서는 MTA 배포 시 db-deployer 모듈이 별도로 정의되어 있어야 합니다.

정리: "HANA Cloud에 CAP를 연결" = 컨테이너 생성 + 스키마 배포 + 앱 바인딩. 셋 중 하나라도 빠지면 연결은 실패합니다.

실전 예제 1단계: 기본 - SalesOrder 최소 모델을 HANA에 올리기

먼저 최소 실패 재현부터 만들어 봅니다. 다음과 같이 매우 단순한 판매 주문 도메인을 정의합니다.

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

entity Customer : cuid, managed {
  name    : String(120);
  email   : String(120);
  orders  : Association to many SalesOrder on orders.customer = $self;
}

entity Product : cuid {
  sku     : String(40);
  title   : String(200);
  price   : Decimal(11,2);
}

entity SalesOrder : cuid, managed {
  orderNo   : String(20);
  customer  : Association to Customer;
  items     : Composition of many SalesOrderItem on items.parent = $self;
  totalAmt  : Decimal(13,2);
}

entity SalesOrderItem : cuid {
  parent    : Association to SalesOrder;
  product   : Association to Product;
  quantity  : Integer;
  netAmt    : Decimal(13,2);
}
// srv/sales-service.cds
using { sales } from '../db/schema';

service SalesService @(path:'/sales') {
  entity Orders     as projection on sales.SalesOrder;
  entity Customers  as projection on sales.Customer;
  entity Products   as projection on sales.Product;
}

package.json에는 다음과 같이 hana 프로필을 미리 정의해 둡니다.

{
  "name": "sales-cap",
  "dependencies": {
    "@sap/cds": "^8",
    "@sap/hana-client": "^2",
    "express": "^4"
  },
  "cds": {
    "requires": {
      "db": {
        "[development]": { "kind": "sqlite", "credentials": { "url": "db.sqlite" } },
        "[production]":  { "kind": "hana", "deploy-format": "hdbtable" }
      }
    }
  }
}

여기서 실패를 먼저 재현해 보겠습니다. cf push만 하고 앱에 접속하면 다음과 같은 오류를 보게 됩니다.

[cds] - failed to connect to database
Error: [401]: authentication failed
  at SQLError (hana-client)
# 또는
invalid table name: Could not find table/view SALES_SALESORDER

이 두 에러 중 하나가 나오면 원인은 100% "HDI 컨테이너에 스키마 배포가 안 됐다"입니다. 인증 실패는 서비스 바인딩이 없거나 컨테이너의 앱 유저가 아직 생성되지 않은 상태이고, invalid table name은 컨테이너는 있는데 .hdbtable이 배포되지 않은 상태입니다.

정상 해결 커맨드는 다음과 같습니다.

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

# 1) 프로젝트 루트에서 실행 — 컨테이너 생성 + 스키마 배포 + default-env.json 기록
cds deploy --to hana:sales-db --store-credentials

--store-credentials 옵션이 있어야 로컬에서도 하이브리드 실행이 가능해집니다. 완료 후 cf services를 실행하면 sales-dbhana-cloud / hdi-shared로 잡혀 있어야 하고, 프로젝트에 default-env.json이 생성됩니다.

실전 예제 2단계: 하이브리드 로깅과 에러 대응

실무에서는 배포 로그를 반드시 남겨야 하고, 실패 시 원인을 빠르게 좁혀야 합니다. 다음처럼 cds deploy를 상세 로그와 함께 실행합니다.

DEBUG=deploy,cli,hdi cds deploy --to hana:sales-db --auto-undeploy 2>&1 | tee deploy-$(date +%Y%m%d-%H%M).log

--auto-undeploy는 소스에서 삭제된 엔티티에 대응하는 HANA 아티팩트를 자동 제거합니다. 이 옵션을 켜지 않으면 SalesOrderItem을 리팩터링으로 이름 바꾸었을 때 옛 테이블이 그대로 남아 다음 배포가 "duplicate object" 계열 오류로 실패합니다.

로컬에서 원격 HANA에 붙어 데이터를 확인하려면 하이브리드 모드로 실행합니다.

cds watch --profile hybrid
# 내부적으로 default-env.json 의 hana 접속정보를 사용

Node.js 서비스 핸들러에서는 배포/연결 상태를 초기화 시점에 로깅해 두면 운영 사고 시 원인 추적이 빨라집니다.

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

module.exports = cds.service.impl(async function () {
  const { Orders } = this.entities;

  this.on('READ', Orders, async (req) => {
    try {
      const tx = cds.tx(req);
      const rows = await tx.run(SELECT.from(Orders).limit(50));
      LOG.info(`Orders fetched: ${rows.length}`);
      return rows;
    } catch (e) {
      // HDI 미배포 시 여기서 'invalid table name' 이 잡힘
      LOG.error('READ Orders failed', { code: e.code, message: e.message });
      req.error(503, `DB not ready: ${e.message}`);
    }
  });

  cds.on('connect', (srv) => {
    if (srv.name === 'db') {
      LOG.info('DB connected', { kind: srv.kind, tenant: srv.options?.credentials?.schema });
    }
  });
});

에러 코드별 트리아지 표를 팀 위키에 붙여두면 좋습니다. 259(invalid table name)는 HDI 배포 누락, 10(authentication failed)은 서비스 바인딩 문제, -813(connection timeout)은 Allowlist/네트워크 문제입니다.

실전 예제 3단계: 프로덕션 — MTA와 CI/CD로 재현 가능하게 만들기

로컬에서 cds deploy로 성공했다고 프로덕션이 되지는 않습니다. 팀 개발과 자동화를 위해서는 MTA(Multi-Target Application) 형태로 db-deployer 모듈을 분리해야 합니다. 다음은 실제로 동작하는 최소 mta.yaml 예시입니다.

_schema-version: '3.3'
ID: sales-cap
version: 1.0.0
parameters:
  enable-parallel-deployments: true

modules:
  - name: sales-srv
    type: nodejs
    path: gen/srv
    requires:
      - name: sales-db
      - name: sales-uaa
    parameters:
      buildpack: nodejs_buildpack
      memory: 512M
    build-parameters:
      builder: npm-ci

  - name: sales-db-deployer
    type: hdb
    path: gen/db
    requires:
      - name: sales-db
    parameters:
      buildpack: nodejs_buildpack
      no-route: true
      no-start: false
      task: true

resources:
  - name: sales-db
    type: com.sap.xs.hdi-container
    parameters:
      service: hana-cloud
      service-plan: hdi-shared
  - name: sales-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service: xsuaa
      service-plan: application

task: true가 핵심입니다. DB Deployer는 상주 앱이 아니라 일회성 태스크로 실행되어야 스키마 반영만 하고 종료합니다. 이 설정이 없으면 매 배포마다 유휴 컨테이너가 남아 리소스를 소모합니다.

빌드와 배포 스크립트는 다음과 같이 구성합니다.

# 1) MTA 빌드 (cds build 자동 호출됨)
mbt build -p=cf

# 2) 프로덕션 배포 — db-deployer 가 hdi 배포까지 한번에 수행
cf deploy mta_archives/sales-cap_1.0.0.mtar -f

# 3) 배포 후 검증
cf run-task sales-srv --command "node -e \"require('@sap/cds').connect.to('db').then(db=>db.run('SELECT COUNT(*) FROM sales_Customer')).then(r=>console.log(r))\""

GitHub Actions 등 CI에서는 서비스 어카운트(테크니컬 유저)로 cf apicf authcf deploy 순서로 실행하고, 배포 실패 시 cf logs sales-db-deployer --recent를 자동 수집하도록 after_failure 훅에 걸어두면 사후 분석이 편합니다.

보안 관점에서는 (1) HANA Cloud Allowlist에 CI Runner의 IP 대역만 허용, (2) 서비스 키는 배포 후 cf delete-service-key로 즉시 파기, (3) default-env.json.gitignore에 반드시 등록해야 합니다. 이 파일이 저장소에 커밋되면 HDI 컨테이너의 자격증명이 그대로 유출됩니다.

흔한 실수와 트러블슈팅 FAQ

실무에서 반복 관찰되는 실패 패턴을 정리했습니다.

  • Q1. cds deploy --to hana가 "No service instance of plan hdi-shared found"로 실패합니다.
    A. 대상 스페이스에 HANA Cloud 인스턴스가 있어도 계약된 엔타이틀먼트가 부족하면 hdi-shared 플랜이 활성화되지 않은 경우가 있습니다. BTP Cockpit → Entitlements에서 hana-cloud / hdi-shared 할당량이 스페이스 레벨까지 내려와 있는지 확인하세요.
  • Q2. 배포는 성공했는데 앱에서 invalid table name: SALES_SALESORDER가 계속 납니다.
    A. 앱이 잘못된 컨테이너에 바인딩된 경우가 대부분입니다. cf env sales-srv | grep -A5 hana로 실제 바인딩된 스키마를 확인하고, 방금 배포한 컨테이너와 이름이 일치하는지 검증하세요. 이전 프로젝트의 컨테이너가 남아 있으면 cds deploy가 그쪽으로 붙어버리기도 합니다.
  • Q3. 컬럼 타입을 변경했더니 "cannot alter table: incompatible type change" 오류가 납니다.
    A. HANA HDI는 안전을 위해 파괴적 변경을 기본 차단합니다. db/undeploy.json에 해당 아티팩트 패턴을 추가하고 cds deploy --auto-undeploy를 실행하거나, 마이그레이션 스크립트(.hdbmigrationtable)로 명시적 버전 관리하세요. 프로덕션 데이터가 있다면 반드시 백업 후 진행합니다.
  • Q4. 로컬에서 cds watch --profile hybrid가 "getaddrinfo ENOTFOUND"로 실패합니다.
    A. HANA Cloud Allowlist에 로컬 IP가 없거나, 회사 프록시가 443을 막고 있을 가능성이 큽니다. curl -v telnet://<hana-host>:443으로 소켓 레벨부터 확인하세요.
  • Q5. MTA 배포 시 db-deployerERR failed to deploy로 죽고 로그가 잘려 보입니다.
    A. cf logs sales-db-deployer --recent는 최근 로그만 보여줍니다. 상세 진단은 cf ssh 또는 배포 중 cf tail-logs 대신 로그를 파일로 리다이렉트해서 잡거나, HDI_DEPLOY_OPTIONS='--verbose' 환경변수를 배포 모듈에 지정하세요.

이 다음에 확장할 만한 주제

배포가 안정화되면 다음 단계는 (1) .hdbmigrationtable을 이용한 스키마 버전 관리, (2) 멀티테넌시를 위한 MTX(Managed Multi-Tenancy)와 subscriber 컨테이너 자동 프로비저닝, (3) HANA Cloud Data Lake 연동으로 대용량 분석 데이터 분리, (4) BAS(Business Application Studio)와 GitHub Actions를 조합한 완전 자동화된 delivery pipeline 구축입니다. 특히 멀티테넌시로 넘어가면 cds deploy는 각 tenant onboarding 시점에 cds-mtxs가 대신 호출하게 되므로, 이 글에서 다룬 3단계의 이해가 기반이 됩니다.

더 읽을거리와 링크

댓글 0

아직 댓글이 없습니다.