SAP BTP - Day1. SAP BTP 핵심 개념 + 첫 번째 CAP 프로젝트

이우철·2026년 4월 24일

SAP_BTP

목록 보기
2/11

[1일차] SAP BTP 핵심 개념 + 첫 번째 CAP 프로젝트

목표: BTP가 왜 존재하는지 이해하고, 실제로 OData API가 동작하는 것을 눈으로 확인한다.
시나리오: 5일간 만들 "출장비 승인 앱"의 뼈대를 세운다.
소요 시간: 이론 2.5시간 + 실습 2.5시간


1일차 학습 목표

이론:
  SAP의 두 가지 세계(온프레미스 vs 클라우드)와 BTP의 위치 이해
  Clean Core 전략이 왜 중요한지 설명할 수 있다
  BTP 4대 기둥과 오늘 배울 영역의 위치를 파악한다
  Cloud Foundry와 Kyma의 차이를 한 줄로 설명할 수 있다

실습:
  CAP 프로젝트 생성 및 CDS Hello World 작성
  cds watch로 로컬 서버 실행
  OData 엔드포인트 브라우저에서 확인
  Git에 1일차 결과 커밋

이론 세션 (2.5시간)


이론 1. 왜 BTP인가? — "문제"부터 시작하기

Before BTP: 전통적인 SAP 확장의 문제점

SAP S/4HANA를 기업에서 도입하면 거의 반드시 커스터마이징이 필요합니다.
전통적인 방법은 ABAP으로 S/4HANA 내부를 직접 수정하는 것이었습니다.

[전통적 방법 — In-System Customization]

S/4HANA Core
├── 표준 모듈 (FI, MM, SD...)
├── 커스텀 ABAP 코드 (Z-program, User Exit, BAdI...)
│       ↑
│    여기에 사업 요구사항을 직접 구현
└── 표준 + 커스텀 코드가 뒤섞임

이 방법의 문제:

문제설명
업그레이드 공포SAP 버전 업그레이드 시 커스텀 코드 전체 재검증 필요
복잡도 폭발10년 된 시스템은 누가 뭘 만들었는지 아무도 모름
기술 종속성ABAP 개발자만 유지보수 가능 → 인력 확보 어려움
클라우드 전환 불가수정된 시스템을 SaaS로 마이그레이션 불가

After BTP: Clean Core + Side-by-Side 확장

[BTP 방식 — Side-by-Side Extension]

S/4HANA Core (표준 유지 — Clean!)
├── 표준 모듈 (FI, MM, SD...)
└── 표준 API만 노출 (OData, RFC)
          ↕ (API 통신)
SAP BTP (확장 레이어)
├── 커스텀 비즈니스 로직
├── 커스텀 UI
└── 워크플로우 자동화

핵심 메시지
"S/4HANA는 건드리지 않는다. 확장은 BTP 위에서 API로 연결한다."
이것이 Clean Core 전략입니다.

Clean Core의 장점:

  • S/4HANA 업그레이드가 커스텀 코드에 영향 없음
  • BTP 위의 확장앱은 Node.js, Java, Python 등 현대 언어로 개발
  • SAP이 아닌 일반 클라우드 개발자도 참여 가능
  • 필요 없어진 확장앱은 BTP에서만 삭제하면 됨

이론 2. BTP 4대 기둥 전체 지도

이 강좌에서 다루는 영역:

  • 애플리케이션 개발: CAP Framework, BAS
  • 공통 기반: XSUAA, Destination, HANA Cloud
  • AI & Automation: SAP Build Process Automation

이론 3. 실행 환경: Cloud Foundry vs Kyma

BTP 위에서 앱을 실행하는 두 가지 환경이 있습니다.

구분Cloud Foundry (CF)Kyma (Kubernetes 기반)
패러다임PaaS (Platform as a Service)Container Orchestration
배포 단위Buildpack 기반 앱Docker 컨테이너 / Helm Chart
진입 장벽낮음 (cf push 한 줄)높음 (K8s 지식 필요)
유연성중간매우 높음
비용 효율소규모에 적합대규모에 유리
이 강좌사용개념만 소개

오늘의 결론: Cloud Foundry로 개발을 시작하고, Kyma는 "더 알고 싶을 때"의 다음 단계 강좌를 고민해 보겠습니다.


이론 4. CAP이란 무엇인가?

CAP = Cloud Application Programming Model
SAP이 만든 오픈소스 풀스택 개발 프레임워크입니다.

CAP 없이 OData API 만들기 (순수 Express.js)

// OData API를 직접 구현한다면... (실제로는 수백 줄)
const express = require('express');
const app = express();

// OData 메타데이터 수동 정의
app.get('/TravelRequests/$metadata', (req, res) => {
  res.set('Content-Type', 'application/xml');
  res.send(`<?xml version="1.0" encoding="utf-8"?>
    <edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
      <edmx:DataServices>
        <Schema Namespace="TravelService" ...>
          <!-- 수동으로 모든 엔티티, 타입, 관계 정의 -->
          ...약 200줄...
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>`);
});

// 각 엔드포인트 수동 구현
app.get('/TravelRequests', (req, res) => { ... });
app.get('/TravelRequests(:id)', (req, res) => { ... });
app.post('/TravelRequests', (req, res) => { ... });
// ... 계속

CAP으로 같은 것 만들기

// CAP을 사용하면... (entities.cds — 단 7줄!)
entity TravelRequests {
  key ID        : UUID;
      title     : String;
      amount    : Decimal;
      status    : String;
}
// service.js — CRUD 자동 완성, 추가 로직만 작성
module.exports = cds.service.impl(async function() {
  // 별도 구현 없이도 CRUD 전부 동작!
});

CAP의 핵심 가치: 반복 코드(Boilerplate)를 제거하고 비즈니스 로직에만 집중하게 해줍니다.

CAP이 자동으로 해주는 것:

  • OData V4 API 자동 생성 (메타데이터 포함)
  • 기본 CRUD 처리
  • 입력값 유효성 검사
  • DB 스키마 자동 생성 (SQLite → HANA Cloud 전환 가능)
  • 인증 연동 (@requires 어노테이션 한 줄)

실습 세션 (2.5시간)


실습 1. CAP 프로젝트 생성

1-1. BAS에서 프로젝트 생성

BAS Terminal을 열고 아래를 실행합니다.

# 프로젝트 디렉토리로 이동
cd /home/user/projects/sap-btp-travel-expense

# CAP 프로젝트 초기화
cds init travel-expense-app
cd travel-expense-app

# 의존성 설치
npm install

# 생성된 구조 확인
ls -la

생성된 폴더 구조:

travel-expense-app/
├── app/           ← Fiori UI가 들어갈 곳 (3일차에 사용)
├── db/            ← 데이터 모델 (CDS 파일)
├── srv/           ← 서비스 레이어 (API 정의, 비즈니스 로직)
├── package.json
└── .cdsrc.json    ← CAP 설정 파일

구조 이해 포인트
db/ = 무엇을 저장하나 (테이블 정의)
srv/ = 무엇을 API로 노출하나 (서비스 정의)
app/ = 사용자에게 어떻게 보여주나 (UI)


1-2. 첫 번째 CDS 데이터 모델 작성

db/schema.cds 파일을 새로 만듭니다. (없으면 생성)

// db/schema.cds
namespace com.travel;

using { cuid, managed } from '@sap/cds/common';

// ──────────────────────────────────────────
//  출장비 신청 엔티티
// ──────────────────────────────────────────
entity TravelRequests : cuid, managed {
  title         : String(100)  @mandatory;
  description   : String(500);
  destination   : String(100)  @mandatory;
  departureDate : Date         @mandatory;
  returnDate    : Date         @mandatory;
  amount        : Decimal(10,2) @mandatory;
  currency      : String(3) default 'KRW';
  status        : String(20) default 'Draft';
  requester     : String(100);
}

코드 해설:

  • cuid : 자동으로 UUID를 Primary Key로 추가하는 Aspect
  • managed : createdAt, createdBy, modifiedAt, modifiedBy 자동 추가
  • @mandatory : null 불가 제약 어노테이션
  • Decimal(10,2) : 소수점 2자리까지의 숫자 (금액에 적합)

1-3. 서비스 정의 작성

srv/travel-service.cds 파일을 만듭니다.

// srv/travel-service.cds
using com.travel from '../db/schema';

// 외부에 노출할 서비스 정의
service TravelService @(path: '/travel') {

  // TravelRequests 엔티티를 API로 노출
  entity TravelRequests as projection on travel.TravelRequests;

}

코드 해설:

  • service TravelService @(path: '/travel') : /travel 경로로 OData 서비스 생성
  • projection on : DB 엔티티를 그대로 노출 (필요 시 컬럼 선택/제외 가능)

1-4. 로컬 개발 서버 실행

# 프로젝트 루트에서
cds watch

예상 출력:

cds serve all --with-mocks --in-memory?
...
[cds] - connect to db > sqlite { database: ':memory:' }
[cds] - serving TravelService { path: '/travel', impl: 'srv/travel-service.js' }
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - launched at 01/01, 11:00:00, version: 7.x.x, in: 1.234s
[cds] - [ terminate with ^C ]

cds watch의 마법
파일을 저장할 때마다 서버를 자동으로 재시작합니다.
개발 중에는 이 명령어를 항상 켜두세요.


1-5. 브라우저에서 API 확인

BAS에서 실행 중인 서버에 접속합니다.

방법 1 (BAS):
오른쪽 하단 팝업 "A service is listening to port 4004" → "Open in New Tab" 클릭

방법 2 (VS Code 로컬):
브라우저에서 http://localhost:4004 직접 접속

확인할 화면:

Welcome to cds.services
─────────────────────────────────
TravelService
  - /travel/$metadata           ← OData 메타데이터
  - /travel/TravelRequests      ← 데이터 목록 API

각 링크를 클릭하여 응답을 확인해보세요.

/travel/TravelRequests 응답 (데이터 없음):

{
  "@odata.context": "$metadata#TravelRequests",
  "value": []
}

/travel/$metadata 응답 (OData 메타데이터 XML):

<EntityType Name="TravelRequests">
  <Key><PropertyRef Name="ID"/></Key>
  <Property Name="ID" Type="Edm.Guid" Nullable="false"/>
  <Property Name="title" Type="Edm.String" MaxLength="100"/>
  <Property Name="amount" Type="Edm.Decimal" .../>
  ...
</EntityType>

축하합니다! CDS 파일 몇 줄로 완전한 OData V4 API가 만들어졌습니다.


실습 2. Mock 데이터 추가 및 API 탐색

2-1. Mock 데이터 파일 생성

db/data/ 폴더를 만들고 CSV 파일을 추가합니다.

mkdir -p db/data

db/data/com.travel-TravelRequests.csv 파일 생성:

ID,title,description,destination,departureDate,returnDate,amount,currency,status,requester
"11111111-1111-1111-1111-111111111111","도쿄 고객사 미팅","Q4 제품 협의","도쿄, 일본","2025-09-01","2025-09-03",850000,"KRW","Draft","장용기"
"22222222-2222-2222-2222-222222222222","싱가포르 컨퍼런스","SAP TechEd 참가","싱가포르","2025-10-15","2025-10-18",2100000,"KRW","Submitted","김연식"
"33333333-3333-3333-3333-333333333333","서울 본사 출장","월간 전략 회의","서울","2025-08-20","2025-08-20",50000,"KRW","Approved","이우철"

파일명 규칙: [namespace]-[EntityName].csv 형식이어야 CAP이 자동으로 인식합니다.

cds watch가 실행 중이면 파일 저장 시 자동으로 데이터를 로드합니다.

브라우저에서 /travel/TravelRequests 재접속 결과:

{
  "@odata.context": "$metadata#TravelRequests",
  "value": [
    {
      "ID": "11111111-...",
      "title": "도쿄 고객사 미팅",
      "destination": "도쿄, 일본",
      "amount": 850000,
      "status": "Draft",
      ...
    },
    ...
  ]
}


2-2. HTTP 파일로 API 테스트 (REST Client)

test/travel.http 파일을 만듭니다.

mkdir -p test
### travel.http
### ──────────────────────────────────────────
### 변수 설정
@baseUrl = http://localhost:4004/travel

### 1. 전체 목록 조회
GET {{baseUrl}}/TravelRequests
Accept: application/json

###

### 2. 특정 항목 조회
GET {{baseUrl}}/TravelRequests(11111111-1111-1111-1111-111111111111)
Accept: application/json

###

### 3. 필터 조회 ($filter)
GET {{baseUrl}}/TravelRequests?$filter=status eq 'Draft'
Accept: application/json

###

### 4. 정렬 및 페이징 ($orderby, $top, $skip)
GET {{baseUrl}}/TravelRequests?$orderby=amount desc&$top=2&$skip=0
Accept: application/json

###

### 5. 특정 컬럼만 조회 ($select)
GET {{baseUrl}}/TravelRequests?$select=title,amount,status
Accept: application/json

###

### 6. 새 출장 신청 등록 (POST)
POST {{baseUrl}}/TravelRequests
Content-Type: application/json

{
  "title": "부산 파트너사 미팅",
  "destination": "부산",
  "departureDate": "2025-11-01",
  "returnDate": "2025-11-01",
  "amount": 120000,
  "currency": "KRW",
  "requester": "박민준"
}

###

### 7. 항목 수정 (PATCH)
PATCH {{baseUrl}}/TravelRequests(11111111-1111-1111-1111-111111111111)
Content-Type: application/json

{
  "status": "Submitted"
}

###

### 8. 항목 삭제 (DELETE)
DELETE {{baseUrl}}/TravelRequests(11111111-1111-1111-1111-111111111111)

###

BAS에서 실행 방법:
### 구분 블록 위의 "Send Request" 버튼 클릭

VS Code에서 실행 방법:
REST Client 익스텐션 설치 후 동일하게 "Send Request" 클릭

OData 쿼리 옵션 실습 포인트

  • $filter: SQL의 WHERE와 동일
  • $orderby: SQL의 ORDER BY와 동일
  • $top/$skip: 페이징 처리
  • $select: SQL의 SELECT 컬럼 지정

이 옵션들은 Fiori UI가 자동으로 생성해서 보내는 것들입니다.


실습 3. 서비스 로직 추가 (비즈니스 로직 첫 맛보기)

srv/travel-service.js 파일을 만들어 간단한 유효성 검사를 추가합니다.

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

module.exports = cds.service.impl(async function () {

  const { TravelRequests } = this.entities;

  // ── Before Hook: 생성 전 유효성 검사 ──────────────────
  this.before('CREATE', TravelRequests, async (req) => {
    const { departureDate, returnDate, amount } = req.data;

    // 출발일이 귀국일보다 늦으면 에러
    if (departureDate > returnDate) {
      return req.error(400, '출발일은 귀국일보다 이전이어야 합니다.');
    }

    // 금액이 0 이하면 에러
    if (amount <= 0) {
      return req.error(400, '출장비 금액은 0보다 커야 합니다.');
    }
  });

  // ── After Hook: 생성 후 처리 ───────────────────────────
  this.after('CREATE', TravelRequests, (result, req) => {
    console.log(`✅ 새 출장 신청 등록: ${result.title} (신청자: ${result.requester})`);
  });

  // ── Custom Action: 상태 변경 (제출) ───────────────────
  this.on('submit', async (req) => {
    const { ID } = req.data;

    const request = await SELECT.one.from(TravelRequests).where({ ID });

    if (!request) {
      return req.error(404, `ID가 ${ID}인 출장 신청을 찾을 수 없습니다.`);
    }

    if (request.status !== 'Draft') {
      return req.error(409, `현재 상태(${request.status})에서는 제출할 수 없습니다.`);
    }

    await UPDATE(TravelRequests).set({ status: 'Submitted' }).where({ ID });
    return { message: '제출이 완료되었습니다.', status: 'Submitted' };
  });

});

서비스 정의에 Action 추가 (srv/travel-service.cds):

// srv/travel-service.cds
using com.travel from '../db/schema';

service TravelService @(path: '/travel') {

  entity TravelRequests as projection on travel.TravelRequests;

  // 상태를 'Submitted'로 변경하는 Custom Action
  action submit(ID: UUID) returns { message: String; status: String; };

}

테스트 — test/travel.http에 추가:

### 9. Custom Action: 제출 처리
POST {{baseUrl}}/TravelRequests(22222222-2222-2222-2222-222222222222)/submit
Content-Type: application/json

{}

###

### 10. 유효성 검사 실패 테스트 (날짜 역전)
POST {{baseUrl}}/TravelRequests
Content-Type: application/json

{
  "title": "잘못된 날짜 테스트",
  "destination": "서울",
  "departureDate": "2025-11-05",
  "returnDate": "2025-11-01",
  "amount": 100000
}

###

실습 4. Git 커밋

# 프로젝트 루트에서
git add .
git commit -m "day1: CAP 프로젝트 초기 설정 및 TravelRequests 엔티티/서비스 정의"
git push origin main

# 다음 날을 위한 브랜치 생성
git checkout -b day2-start
git push origin day2-start
git checkout main

의도적 에러 실습: 자주 하는 실수 경험해보기

이 섹션은 에러를 직접 만들고 고치는 훈련입니다.

에러 1: CDS 파일 문법 오류

db/schema.cds에서 세미콜론을 하나 지워보세요.

// 잘못된 코드 — 세미콜론 없음
entity TravelRequests : cuid, managed {
  title : String(100) @mandatory   // ← 여기 세미콜론 없음
  amount : Decimal(10,2);
}

에러 메시지 읽기:

[ERROR] db/schema.cds:5:3: Extraneous input 'amount' expecting...
  • 5:3 = 5번 줄, 3번째 열에서 에러 발생
  • "Extraneous input" = 예상치 못한 토큰이 나타남

고치기: 세미콜론 복원


에러 2: CSV 파일명 오류

db/data/ 의 CSV 파일명을 TravelRequest.csv (s 없음)로 바꾸면?

[WARN] can't find a match for TravelRequest.csv

데이터가 로드되지 않고 목록이 비어있습니다.

고치기: 파일명을 com.travel-TravelRequests.csv 로 복원


1일차 마무리 체크

확인 항목:
[ ] cds watch 실행 시 에러 없이 서버 시작됨
[ ] 브라우저에서 /travel/TravelRequests 로 3개 Mock 데이터 확인
[ ] HTTP 파일로 CRUD 테스트 성공
[ ] submit Action 호출 성공
[ ] 유효성 검사 실패 시 에러 메시지 반환 확인
[ ] Git 커밋 완료

Q&A 주제 (강사에게 물어보기):
[ ] cuid vs UUID를 직접 정의하는 것의 차이?
[ ] Aspect(cuid, managed)를 직접 만들려면?
[ ] OData V2와 V4의 실질적 차이?

1일차 핵심 정리

오늘 배운 것:
┌─────────────────────────────────────────────┐
│  BTP = S/4HANA 외부에서 API로 확장하는 플랫폼│
│                                             │
│  CAP = CDS(데이터 모델) + JS(로직) 조합으로  │
│        OData API를 자동 생성하는 프레임워크  │
│                                             │
│  cds watch = 저장 즉시 반영되는 개발 루프    │
│                                             │
│  OData 쿼리 = $filter, $orderby, $select... │
└─────────────────────────────────────────────┘

내일 할 것:
→ CDS 모델을 더 정교하게 (Association, Enum)
→ Mock 데이터로 복잡한 API 쿼리 마스터
→ 로컬 SQLite → HANA Cloud 연결 준비

다음: [2일차] CDS 고급 모델링 + 완전한 데이터 레이어 구축

profile
개발 정리 공간 - 업무일때도 있고, 공부일때도 있고...

0개의 댓글