목표: BTP가 왜 존재하는지 이해하고, 실제로 OData API가 동작하는 것을 눈으로 확인한다.
시나리오: 5일간 만들 "출장비 승인 앱"의 뼈대를 세운다.
런타임: CAP Java (Spring Boot 기반) + Java 17 + Maven
소요 시간: 이론 2.5시간 + 실습 2.5시간
이론:
SAP의 두 가지 세계(온프레미스 vs 클라우드)와 BTP의 위치 이해
Clean Core 전략이 왜 중요한지 설명할 수 있다
BTP 4대 기둥과 오늘 배울 영역의 위치를 파악한다
Cloud Foundry와 Kyma의 차이를 한 줄로 설명할 수 있다
CAP Node.js와 CAP Java의 차이, Java를 선택하는 이유
실습:
CAP Java 프로젝트 생성 (Maven Archetype)
CDS Hello World 작성 (Node.js 버전과 동일한 파일!)
mvn spring-boot:run 으로 로컬 서버 실행
OData 엔드포인트 브라우저에서 확인
Git에 1일차 결과 커밋
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로 마이그레이션 불가 |
[BTP 방식 — Side-by-Side Extension]
S/4HANA Core (표준 유지 — Clean!)
├── 표준 모듈 (FI, MM, SD...)
└── 표준 API만 노출 (OData, RFC)
↕ (API 통신)
SAP BTP (확장 레이어)
├── 커스텀 비즈니스 로직 ← Java로 개발!
├── 커스텀 UI
└── 워크플로우 자동화
핵심 메시지
"S/4HANA는 건드리지 않는다. 확장은 BTP 위에서 API로 연결한다."
이것이 Clean Core 전략입니다.
Clean Core + Java 조합의 장점:

| 구분 | Cloud Foundry (CF) | Kyma (Kubernetes 기반) |
|---|---|---|
| 패러다임 | PaaS (Platform as a Service) | Container Orchestration |
| 배포 단위 | Buildpack 기반 앱 | Docker 컨테이너 / Helm Chart |
| 진입 장벽 | 낮음 (cf push 한 줄) | 높음 (K8s 지식 필요) |
| 유연성 | 중간 | 매우 높음 |
| 비용 효율 | 소규모에 적합 | 대규모에 유리 |
| 이 강좌 | 사용 | 개념만 소개 |
오늘의 결론: Cloud Foundry로 개발을 시작하고, Kyma는 "더 알고 싶을 때"의 다음 단계로 시도해 보세요.
CAP = Cloud Application Programming Model
SAP이 만든 오픈소스 풀스택 개발 프레임워크로, Node.js와 Java 두 가지 런타임을 공식 지원합니다.
| 구분 | CAP Node.js | CAP Java |
|---|---|---|
| 기반 | Express.js | Spring Boot |
| 빌드 | npm | Maven |
| 서버 실행 | cds watch | mvn spring-boot:run |
| 핸들러 | .js 파일 | Java 클래스 + 어노테이션 |
| CDS 모델 파일 | .cds 파일 | .cds 파일 (동일!) |
| DI(의존성 주입) | 수동 | Spring DI (@Autowired) |
| 테스트 | Jest | JUnit + MockMvc |
| 회사 적합성 | JS 팀 | Java 개발사 |
핵심 포인트
CDS 데이터 모델 파일(.cds)은 Node.js와 Java가 완전히 동일합니다.
바뀌는 것은 서비스 핸들러 코드뿐입니다.
mvn compile 시)application.yaml 설정만 변경)@requires 어노테이션)# Java 17 확인
java -version
# 출력 예: openjdk version "17.0.x"
# Maven 확인
mvn -version
# 출력 예: Apache Maven 3.9.x
# CDS CLI 확인 (CDS 도구는 여전히 필요)
cds --version
# 출력 예: @sap/cds: 7.x.x
# Node.js 확인 (CDS 도구 실행에 필요)
node --version
# 출력 예: v18.x.x 이상
왜 Java 프로젝트인데 Node.js가 필요한가?
CDS 파일(.cds)을 컴파일하는cdsCLI 도구는 Node.js 기반입니다.
Maven 빌드 시 내부적으로 CDS 컴파일을 수행합니다.
하지만 개발자는 이 과정을 신경 쓸 필요 없이mvn명령어만 사용합니다.
# 프로젝트를 만들 디렉토리로 이동
cd /home/user/projects/sap-btp-travel-expense
# CAP Java Archetype으로 프로젝트 생성
mvn archetype:generate \
-DarchetypeGroupId=com.sap.cds \
-DarchetypeArtifactId=cds-services-archetype \
-DarchetypeVersion=LATEST \
-DgroupId=com.travel \
-DartifactId=travel-expense-java \
-Dversion=1.0.0 \
-Dpackage=com.travel \
-DinteractiveMode=false
(WIN)
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate `-DarchetypeGroupId=com.sap.cds `-DarchetypeArtifactId=cds-services-archetype `-DarchetypeVersion=LATEST `-DgroupId=com.travel `-DartifactId=travel-expense-java `-Dversion=1.0.0 `-Dpackage=com.travel `-DinteractiveMode=false
# 생성된 프로젝트로 이동
cd travel-expense-java
mvn 실행 시 Groove script 플러그인 호환성 문제로 에러처럼 (BUILD FAIL) 보일 수 있으나 생성된 디렉토리와 파일들을 확인하여 생성에 문제가 없으면 다음으로 넘어갑니다.
생성된 폴더 구조:
travel-expense-java/
├── app/ ← Fiori UI (3일차에 사용)
├── db/ ← 데이터 모델 (CDS 파일 — Node.js와 동일!)
├── srv/ ← 서비스 정의 (CDS 파일 — Node.js와 동일!)
│
├── src/
│ └── main/
│ ├── java/
│ │ └── com/travel/ ← Java 서비스 핸들러 (이것이 .js 대체)
│ └── resources/
│ └── application.yaml ← Spring Boot 설정 (포트, DB 등)
│
├── pom.xml ← Maven 빌드 설정 (package.json 역할)
└── .cdsrc.json ← CAP 설정
| 항목 | 설명 |
|---|---|
| app/ 없음 | 최신 Archetype에서 Fiori 폴더를 기본 생성하지 않음. 3일차 때 직접 만들면 됨. 지금은 불필요 |
| src/가 srv/ 안으로 이동 | CAP Java가 Maven Multi-Module 구조로 변경됨. srv/가 실제 Spring Boot 모듈 |
| srv/pom.xml 추가 | 멀티모듈 Maven 프로젝트의 자식 모듈로 pom.xml 추가됨 |
구조 이해 포인트
db/= 무엇을 저장하나 (테이블 정의) — Node.js와 동일
srv/*.cds= 무엇을 API로 노출하나 — Node.js와 동일
srv/src/main/java/= 비즈니스 로직 Java 코드 — Node.js의srv/*.js대체
pom.xml= 의존성 관리 — Node.js의package.json대체
생성된 pom.xml을 열어 주요 의존성을 확인합니다.
<!-- pom.xml 주요 부분 -->
<dependencies>
<!-- CDS SPRING BOOT STARTER -->
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-starter-spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-adapter-odata-v4</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
cds-starter-spring-boot-odata가 하는 일
Spring Boot Auto-configuration으로 OData 서블릿, CDS 서비스 등록,
이벤트 핸들러 스캔을 자동으로 설정해줍니다. 직접 설정할 것이 거의 없습니다.
src/main/resources/application.yaml을 확인합니다. (없으면 생성)
# src/main/resources/application.yaml
server:
port: 8080
spring:
sql:
init:
platform: h2 # schema-h2.sql을 찾아서 H2 DB 초기화 (테이블 생성)
cds:
datasource:
auto-config:
enabled: false
security:
enabled: false
mock:
enabled: false
users:
alice:
password: ""
roles:
- authenticated-user
- Admin
- TravelAdmin
- Requester
- Approver # H2 사용 시 CAP 자동설정 비활성화 (Spring이 schema-h2.sql로 직접 초기화)
포트 변경 주의
CAP Node.js는 기본 포트가4004이고,
CAP Java(Spring Boot)는 기본 포트가8080입니다.
3일차 Fiori 연결 시 이 포트를 사용합니다.
db/schema.cds 파일을 새로 만듭니다. 이 파일은 Node.js 버전과 완전히 동일합니다.
// db/schema.cds
// 이 파일은 CAP Java와 CAP Node.js에서 100% 동일합니다!
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로 추가하는 Aspectmanaged:createdAt,createdBy,modifiedAt,modifiedBy자동 추가@mandatory: null 불가 제약 어노테이션Decimal(10,2): 소수점 2자리까지의 숫자 (금액에 적합)
srv/travel-service.cds 파일을 만듭니다. 이 파일도 Node.js 버전과 완전히 동일합니다.
// srv/travel-service.cds
// 이 파일도 CAP Java와 CAP Node.js에서 100% 동일합니다!
using com.travel from '../db/schema';
// 외부에 노출할 서비스 정의
service TravelService @(path: '/travel') {
// TravelRequests 엔티티를 API로 노출
entity TravelRequests as projection on travel.TravelRequests;
// 상태를 'Submitted'로 변경하는 Custom Action (1일차 실습용)
action submit(ID: UUID) returns { message: String; status: String; };
}
npm install 하고 나서 mvn compile 하세요
# 프로젝트 루트에서 — CDS 컴파일 + Java 소스 생성 + 빌드
mvn compile
# 빌드 성공 후 생성된 파일 확인
ls srv/target/classes/cds/gen/travelservice/
# 출력 예:
# TravelRequests.java ← 엔티티 POJO 인터페이스 (자동 생성!)
# TravelRequests_.java ← CQN 쿼리 빌더 헬퍼 (자동 생성!)
# TravelService_.java ← 서비스 상수 클래스 (자동 생성!)

CAP Java의 마법: 자동 POJO 생성
mvn compile을 실행하면 CDS 파일을 읽어 Java 인터페이스를 자동 생성합니다.
TravelRequests인터페이스에는getTitle(),setStatus()등이 이미 만들어져 있습니다.
개발자는 이 클래스를 직접 만들 필요가 없습니다.
생성된 TravelRequests.java (참고용 — 직접 수정 금지):
srv/src/gen/java/cds/gen/travelservice/TravelRequests.java 에 위치
// target/generated-sources/...에 자동 생성됨
// 이 파일은 수정하지 마세요! mvn compile 시 덮어써집니다.
package cds.gen.travelservice;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import java.lang.String;
import java.math.BigDecimal;
public interface TravelRequests extends cds.gen.com.travel.TravelRequests {
String getTitle();
void setTitle(String title);
String getStatus();
void setStatus(String status);
BigDecimal getAmount();
void setAmount(BigDecimal amount);
// ... 나머지 필드들도 자동 생성
}
# Spring Boot 개발 서버 실행
mvn spring-boot:run
예상 출력:
_________ ____ __
/ ____/ | / __ \ / /___ __ ______ _
/ / / /| | / /_/ / __ / / __ `/ | / / __ `/
/ /___/ ___ |/ ____/ / /_/ / /_/ /| |/ / /_/ /
\____/_/ |_/_/ \____/\__,_/ |___/\__,_/ 4.9.0
Powered by Spring Boot 3.5.13
2026-05-03T07:38:07.889+09:00 INFO 11624 --- [ restartedMain] com.travel.Application : Starting Application using Java 17.0.18 with PID 11624 (C:\dev\space\sap_btp\sap-btp-travel-expense\travel-expense-java\srv\target\classes started by wclee in C:\dev\space\sap_btp\sap-btp-travel-expense\travel-expense-java\srv)
2026-05-03T07:38:07.892+09:00 INFO 11624 --- [ restartedMain] com.travel.Application : No active profile set, falling back to 1 default profile: "default"
2026-05-03T07:38:07.953+09:00 INFO 11624 --- [ restartedMain] o.s.b.devtools.restart.ChangeableUrls : The Class-Path manifest attribute in C:\Users\wclee\.m2\repository\com\cronutils\cron-utils\9.2.1\cron-utils-9.2.1.jar referenced one or more files that do not exist: file:/C:/Users/wclee/.m2/repository/com/cronutils/cron-utils/9.2.1/slf4j-api-2.0.7.jar
2026-05-03T07:38:07.954+09:00 INFO 11624 --- [ restartedMain] .e.DevToolsPro
Node.js의
cds watch와 비교
- Node.js:
cds watch(저장 즉시 자동 재시작)- Java:
mvn spring-boot:run(수동 재시작 필요)
개발 중 CDS 파일 변경 시에는Ctrl+C로 종료 후 재실행이 필요합니다.
브라우저에서 http://localhost:8080 접속:
Welcome to cds.services
─────────────────────────────────
TravelService
- /travel/$metadata ← OData 메타데이터
- /travel/TravelRequests ← 데이터 목록 API
Java CAP:
http://localhost:8080/odata/v4

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


축하합니다! CDS 파일 몇 줄 + Java 프로젝트 설정만으로
완전한 OData V4 API가 Spring Boot 위에서 동작합니다.
Node.js 버전과 완전히 동일한 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","이철수"
# DB 초기화 (CSV 데이터를 SQLite에 로드)
# CDS 도구로 DB 파일 생성
mvn spring-boot:run
# 서버 시작 시 자동으로 CSV 데이터를 DB에 로드합니다
test/travel.http 파일을 만듭니다. (Node.js 버전과 URL만 포트 다름)
### travel_java.http — 1일차 CAP Java API 테스트
### ──────────────────────────────────────────
### 변수 설정
@baseUrl = http://localhost:8080/odata/v4/travel
@auth = Basic alice:
### 1. 전체 목록 조회
GET {{baseUrl}}/TravelRequests
Authorization: {{auth}}
Accept: application/json
###
### 2. 특정 항목 조회
GET {{baseUrl}}/TravelRequests(ID=22222222-2222-2222-2222-222222222222,IsActiveEntity=true)
Authorization: {{auth}}
Accept: application/json
###
### 3. 필터 조회 ($filter)
GET {{baseUrl}}/TravelRequests?$filter=status eq 'Draft'
Authorization: {{auth}}
Accept: application/json
###
### 4. 정렬 및 페이징 ($orderby, $top, $skip)
GET {{baseUrl}}/TravelRequests?$orderby=totalAmount desc&$top=2&$skip=0
Authorization: {{auth}}
Accept: application/json
###
### 5. 특정 컬럼만 조회 ($select)
GET {{baseUrl}}/TravelRequests?$select=title,totalAmount,status
Authorization: {{auth}}
Accept: application/json
###
### 6. 새 출장 신청 등록 (POST)
POST {{baseUrl}}/TravelRequests
Authorization: {{auth}}
Content-Type: application/json
{
"ID": "44444444-4444-4444-4444-444444444444",
"title": "부산 파트너사 미팅",
"destination": "부산",
"departureDate": "2025-11-01",
"returnDate": "2025-11-01",
"currency": "KRW",
"requester": "박민준"
}
###
### 7. 항목 수정 (PATCH) - 제목 수정
PATCH {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)
Authorization: {{auth}}
Content-Type: application/json
{
"title": "부산 파트너사 미팅 (수정됨3)"
}
###
### 8. Custom Action: 제출
POST {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)/TravelService.submit
Authorization: {{auth}}
Content-Type: application/json
{}
###
### 9. Custom Action: 승인
POST {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)/TravelService.approve
Authorization: {{auth}}
Content-Type: application/json
{
"comment": "승인됩니다"
}
###
### 10. Custom Action: 반려
# 참고: 9번(승인)을 실행한 후에는 반려할 수 없습니다. 반려 테스트 시에는 6번(새로 생성) -> 8번(제출) -> 10번(반려) 순서로 진행하세요.
POST {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)/TravelService.reject
Authorization: {{auth}}
Content-Type: application/json
{
"comment": "출장 사유가 명확하지 않습니다"
}
###
### 11. 항목 삭제 (DELETE)
DELETE {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)
Authorization: {{auth}}
Node.js 버전과 차이점
URL에서localhost:4004→localhost:8080으로만 바뀌었습니다.
OData 쿼리 옵션($filter,$orderby,$select등)은 완전히 동일합니다.
이것이 Node.js와 가장 크게 다른 부분입니다.
src/main/java/com/travel/handlers/ 디렉토리를 만들고 Java 클래스를 작성합니다.
mkdir -p src/main/java/com/travel/handlers
src/main/java/com/travel/handlers/TravelServiceHandler.java 생성:
package com.travel.handlers;
import cds.gen.travelservice.TravelRequests;
import cds.gen.travelservice.TravelRequests_;
import cds.gen.travelservice.TravelService_;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.After;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.persistence.PersistenceService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
/**
* TravelService 이벤트 핸들러
*
* Node.js: module.exports = cds.service.impl(async function () { ... })
* Java: @Component + @ServiceName 어노테이션으로 동일한 역할
*/
@Component
@ServiceName(TravelService_.CDS_NAME)
public class TravelServiceHandler implements EventHandler {
private static final Logger log = LoggerFactory.getLogger(TravelServiceHandler.class);
// Node.js: const { TravelRequests } = this.entities;
// Java: Spring이 자동으로 주입해줌 (의존성 주입)
@Autowired
private PersistenceService db;
// ════════════════════════════════════════════════════
// BEFORE Hook — 검증 로직
//
// Node.js: this.before('CREATE', TravelRequests, async (req) => { ... })
// Java: @Before 어노테이션 + 메서드 파라미터로 엔티티 수신
// ════════════════════════════════════════════════════
@Before(event = CqnService.EVENT_CREATE, entity = TravelRequests_.CDS_NAME)
public void validateCreate(TravelRequests request) {
// Node.js: const { departureDate, returnDate, amount } = req.data;
// Java: CAP이 자동으로 TravelRequests 객체를 파라미터에 주입해줌
var departureDate = request.getDepartureDate();
var returnDate = request.getReturnDate();
var amount = request.getAmount();
// 출발일이 귀국일보다 늦으면 에러
if (departureDate != null && returnDate != null
&& departureDate.isAfter(returnDate)) {
// Node.js: return req.error(400, '...');
// Java: throw new ServiceException(...)
throw new ServiceException(ErrorStatuses.BAD_REQUEST,
"출발일은 귀국일보다 이전이어야 합니다.");
}
// 금액이 0 이하면 에러
if (amount != null && amount.signum() <= 0) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST,
"출장비 금액은 0보다 커야 합니다.");
}
}
// ════════════════════════════════════════════════════
// AFTER Hook — 생성 후 로깅
//
// Node.js: this.after('CREATE', TravelRequests, (result, req) => { ... })
// Java: @After 어노테이션 + 결과 엔티티를 파라미터로 수신
// ════════════════════════════════════════════════════
@After(event = CqnService.EVENT_CREATE, entity = TravelRequests_.CDS_NAME)
public void afterCreate(TravelRequests result) {
// Node.js: console.log(`새 출장 신청 등록: ${result.title}`)
// Java: SLF4J Logger 사용 (Spring Boot 표준)
log.info("새 출장 신청 등록: {} (신청자: {})",
result.getTitle(), result.getRequester());
}
// ════════════════════════════════════════════════════
// Custom Action — 상태 변경 (제출)
//
// Node.js: this.on('submit', TravelRequests, async (req) => { ... })
// Java: @On 어노테이션 + Map으로 파라미터 수신
// ════════════════════════════════════════════════════
@On(event = "submit", entity = TravelRequests_.CDS_NAME)
public void onSubmit(Map<String, Object> params,
com.sap.cds.services.EventContext ctx) {
// 요청 대상 엔티티의 ID 추출
// Node.js: const { ID } = req.params[0];
@SuppressWarnings("unchecked")
var keyMap = (Map<String, Object>) ctx.get("$keys");
var id = (String) (keyMap != null ? keyMap.get("ID") : params.get("ID"));
// DB에서 현재 상태 조회
// Node.js: const request = await SELECT.one.from(TravelRequests).where({ ID });
var query = Select.from(TravelRequests_.class)
.where(t -> t.ID().eq(id));
Optional<TravelRequests> optRequest = db.run(query).first(TravelRequests.class);
if (optRequest.isEmpty()) {
throw new ServiceException(ErrorStatuses.NOT_FOUND,
"해당 출장 신청을 찾을 수 없습니다.");
}
var request = optRequest.get();
if (!"Draft".equals(request.getStatus())) {
throw new ServiceException(ErrorStatuses.CONFLICT,
String.format("현재 상태(%s)에서는 제출할 수 없습니다.", request.getStatus()));
}
// 상태 업데이트
// Node.js: await UPDATE(TravelRequests).set({ status: 'Submitted' }).where({ ID });
db.run(Update.entity(TravelRequests_.class)
.data(Map.of("status", "Submitted"))
.where(t -> t.ID().eq(id)));
// 결과 반환
ctx.setResult(Map.of("message", "제출이 완료되었습니다.", "status", "Submitted"));
ctx.setCompleted();
}
}
서버 재실행:
# Ctrl+C로 기존 서버 종료 후
mvn spring-boot:run
테스트 — test/travel.http에 추가:
### 8. Custom Action: 제출
POST {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)/TravelService.submit
Authorization: {{auth}}
Content-Type: application/json
{}
###
### 9. Custom Action: 승인
POST {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)/TravelService.approve
Authorization: {{auth}}
Content-Type: application/json
{
"comment": "승인됩니다"
}
###
### 10. Custom Action: 반려
# 참고: 9번(승인)을 실행한 후에는 반려할 수 없습니다. 반려 테스트 시에는 6번(새로 생성) -> 8번(제출) -> 10번(반려) 순서로 진행하세요.
POST {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)/TravelService.reject
Authorization: {{auth}}
Content-Type: application/json
{
"comment": "출장 사유가 명확하지 않습니다"
}
###
### 11. 항목 삭제 (DELETE)
DELETE {{baseUrl}}/TravelRequests(ID=44444444-4444-4444-4444-444444444444,IsActiveEntity=false)
Authorization: {{auth}}
💡 Node.js 핸들러와 Java 핸들러 비교
개념 Node.js Java 서비스 등록 cds.service.impl()@ServiceName어노테이션이벤트 훅 등록 this.before()@Before어노테이션엔티티 데이터 접근 req.data.titlerequest.getTitle()에러 반환 req.error(400, '...')throw new ServiceException(...)DB 조회 await SELECT.one.from(...)db.run(Select.from(...))로깅 console.log(...)log.info(...)
# .gitignore 생성
cat > .gitignore << 'EOF'
target/
db/travel.db
*.class
.settings/
.project
.classpath
EOF
git init
git add .
git commit -m "day1: CAP Java 프로젝트 초기 설정 및 TravelRequests 엔티티/서비스 정의"
git push origin main
# 다음 날을 위한 브랜치 생성
git checkout -b day2-start
git push origin day2-start
git checkout main
// 잘못된 코드 — 세미콜론 없음
entity TravelRequests : cuid, managed {
title : String(100) @mandatory // ← 여기 세미콜론 없음
amount : Decimal(10,2);
}
에러는 빌드 단계에서 발생합니다:
[ERROR] Failed to execute goal com.sap.cds:cds-maven-plugin:generate
[ERROR] db/schema.cds:5:3: Extraneous input 'amount' expecting...
고치기: 세미콜론 복원 후 mvn spring-boot:run
// 잘못된 import
import com.travel.TravelRequests; // ❌ 이 패키지에는 없음
// 올바른 import (CAP이 자동 생성한 클래스)
import cds.gen.travelservice.TravelRequests; //
CAP Java 자주 하는 실수
자동 생성 클래스는cds.gen.[서비스명소문자]패키지에 있습니다.
빌드 전에는 클래스가 없어 IDE가 빨간 줄을 표시할 수 있습니다.
mvn compile먼저 실행하세요.
[ERROR] cannot find symbol
[ERROR] symbol: class TravelRequests_
[ERROR] location: package cds.gen.travelservice
원인: CDS 파일을 수정했지만 mvn compile을 실행하지 않아 Java 클래스가 갱신되지 않음.
해결:
mvn clean compile # CDS 재컴파일 + Java 클래스 재생성
mvn spring-boot:run
확인 항목:
[ ] mvn spring-boot:run 실행 시 에러 없이 서버 시작됨 (포트 8080)
[ ] 브라우저에서 /travel/TravelRequests 로 3개 Mock 데이터 확인
[ ] HTTP 파일로 CRUD 테스트 성공
[ ] submit Action 호출 성공 (9번 테스트)
[ ] 유효성 검사 실패 시 에러 메시지 반환 확인 (10번 테스트)
[ ] Git 커밋 완료
Q&A 주제 (강사에게 물어보기):
[ ] CAP Java에서 mvn compile 없이 자동 재빌드하는 방법?
[ ] @Component와 @ServiceName의 동작 원리
[ ] PersistenceService vs JPA Repository의 차이?
[ ] OData V2와 V4의 실질적 차이?
오늘 배운 것:
┌────────────────────────────────────────────────────────┐
│ BTP = S/4HANA 외부에서 API로 확장하는 플랫폼 │
│ │
│ CAP Java = CDS(데이터 모델) + Java(Spring Boot 로직) │
│ 조합으로 OData API를 자동 생성 │
│ │
│ CDS 파일(.cds)은 Node.js와 Java에서 완전히 동일! │
│ │
│ Java 핸들러 = @Component + @Before/@After/@On │
│ │
│ Node.js와 Java의 핵심 차이: │
│ - 프로젝트 생성: npm → mvn archetype:generate │
│ - 서버 실행: cds watch → mvn spring-boot:run │
│ - 핸들러: .js 파일 → Java @Component 클래스 │
│ - 포트: 4004 → 8080 │
└────────────────────────────────────────────────────────┘
내일 할 것:
→ CDS 모델을 더 정교하게 (Association, Enum, Composition)
→ Java 핸들러에서 복잡한 비즈니스 로직 구현
→ 상태 전이(submit/approve/reject) Java로 완성
다음: [2일차] CDS 고급 모델링 + 완전한 데이터 레이어 (Java)