ABAP 개발자에게 최근 몇 년간 가장 큰 변화를 꼽으라면 단연 RAP(ABAP RESTful Application Programming Model) 일 것이다. 과거 BOPF나 클래식 다이나믹 프로그래밍으로 만들던 트랜잭션 애플리케이션이, 이제는 CDS 뷰와 선언적 동작 정의(Behavior Definition)로 대체되었다.
특히 S/4HANA 클린 코어(Clean Core) 전략과 ABAP Cloud가 표준이 되면서, RAP은 “선택 사항”이 아니라 “기본기”가 되었다. 이 글에서는 RAP 비즈니스 오브젝트(Business Object, 이하 BO)를 밑바닥부터 만들어 Fiori Elements 화면까지 띄우는 전 과정을 정리한다.
목차
1. RAP 아키텍처를 먼저 이해하자
RAP BO는 여러 개의 개발 오브젝트가 레이어를 이루는 구조다. 순서대로 쌓아 올린다고 생각하면 된다.
[Service Binding] ← OData V2/V4 프로토콜 노출, Fiori 프리뷰
↑
[Service Definition] ← 어떤 엔티티를 외부에 공개할지 정의
↑
[Projection Layer] ← ZC_ 프로젝션 뷰 + 프로젝션 BDL (소비 계층)
↑
[Behavior Definition] ← BDL: create/update/delete/action/validation 선언
[Behavior Implementation] ← ABAP 클래스: 실제 로직 구현
↑
[Data Model] ← ZR_ CDS 루트 뷰 엔티티 (+ composition 자식)
↑
[Database Table] ← 영속 테이블, Draft 테이블
핵심은 선언(Declaration)과 구현(Implementation)의 분리다. BDL 파일에서 “이 BO는 생성/수정/삭제가 가능하고, 저장 시점에 고객번호 검증을 한다”고 선언하면, 프레임워크가 그에 맞는 메서드 시그니처를 만들어주고 개발자는 로직만 채운다.
구현 시나리오 3가지
| 시나리오 | 설명 | 사용 시점 |
|---|---|---|
| Managed | 프레임워크가 DB 저장까지 전부 처리 | 신규 개발 (그린필드) |
| Unmanaged | 개발자가 저장 로직까지 직접 구현 | 기존 레거시 로직 재사용 |
| Managed with unmanaged save | 조회/버퍼는 프레임워크, 저장만 직접 | 기존 함수모듈 저장 로직 활용 |
신규 개발이라면 고민 없이 Managed를 선택하면 된다. 이 글도 Managed 기준으로 진행한다.
2. 사전 준비
- Eclipse + ADT(ABAP Development Tools) — RAP은 SAP GUI에서 개발할 수 없다. ADT 필수.
- 시스템: S/4HANA 2020 이상 (온프레미스), 또는 SAP BTP ABAP Environment(Steampunk), ABAP Trial 환경
- 언어 버전: ABAP for Cloud Development 권장 (릴리즈된 API만 사용 가능)
- 데모 데이터를 쓰려면
/DMO/FLIGHT_DATA_GENERATOR프로그램을 먼저 실행해 항공편 샘플 데이터를 생성해 둔다.
예제로는 SAP 공식 데모와 동일한 여행 예약(Travel) 시나리오를 사용한다. 접미사 _JSC는 각자의 이니셜로 바꾸면 된다.
3. Step 1 — 영속 테이블(Persistent Table) 정의
먼저 데이터가 실제로 저장될 DB 테이블을 만든다. RAP에서 중요한 것은 관리 필드(Administrative Fields) 다. abp_ 로 시작하는 전용 데이터 엘리먼트를 써야 프레임워크가 자동으로 값을 채워준다.
abap
@EndUserText.label : 'Travel Data'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table ztravel_jsc {
key client : abap.clnt not null;
key travel_uuid : sysuuid_x16 not null;
travel_id : /dmo/travel_id;
agency_id : /dmo/agency_id;
customer_id : /dmo/customer_id;
begin_date : /dmo/begin_date;
end_date : /dmo/end_date;
@Semantics.amount.currencyCode : 'ztravel_jsc.currency_code'
booking_fee : /dmo/booking_fee;
@Semantics.amount.currencyCode : 'ztravel_jsc.currency_code'
total_price : /dmo/total_price;
currency_code : /dmo/currency_code;
description : /dmo/description;
overall_status : /dmo/overall_status;
created_by : abp_creation_user;
created_at : abp_creation_tstmpl;
last_changed_by : abp_lastchange_user;
last_changed_at : abp_lastchange_tstmpl;
local_last_changed_at : abp_locinst_lastchange_tstmpl;
}
관리 필드가 두 개인 이유
처음 보면 헷갈리는 부분인데, 변경 타임스탬프가 두 개다.
last_changed_at(Total ETag): 루트를 포함해 자식 엔티티까지 어디든 변경되면 갱신. OData 클라이언트의 동시성 제어용.local_last_changed_at(ETag Master / Local ETag): 해당 인스턴스 자체가 변경될 때만 갱신. 낙관적 잠금(Optimistic Locking)용.
자식 엔티티(Booking)에는 local_last_changed_at 만 있으면 된다. Total ETag는 루트에만 존재한다.
4. Step 2 — CDS 루트 뷰 엔티티 정의
DB 테이블 위에 데이터 모델을 얹는다. RAP에서는 define root view entity 를 쓴다. 구식 define view (DDIC 기반 뷰)가 아니라 View Entity 를 쓰는 것이 중요하다.
abap
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Travel - Root View Entity'
define root view entity ZR_TRAVEL_JSC
as select from ztravel_jsc as Travel
composition [0..*] of ZR_BOOKING_JSC as _Booking
association [0..1] to /DMO/I_Agency as _Agency on $projection.AgencyID = _Agency.AgencyID
association [0..1] to /DMO/I_Customer as _Customer on $projection.CustomerID = _Customer.CustomerID
association [0..1] to I_Currency as _Currency on $projection.CurrencyCode = _Currency.Currency
{
key travel_uuid as TravelUUID,
travel_id as TravelID,
agency_id as AgencyID,
customer_id as CustomerID,
begin_date as BeginDate,
end_date as EndDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
booking_fee as BookingFee,
@Semantics.amount.currencyCode: 'CurrencyCode'
total_price as TotalPrice,
currency_code as CurrencyCode,
description as Description,
overall_status as OverallStatus,
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.lastChangedBy: true
last_changed_by as LastChangedBy,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
local_last_changed_at as LocalLastChangedAt,
/* Associations */
_Booking,
_Agency,
_Customer,
_Currency
}
Composition vs Association
RAP BO의 계층 구조는 composition 으로 만든다. Association과의 차이는 명확하다.
- Composition: 부모-자식 종속 관계. 부모가 삭제되면 자식도 삭제된다. BO의 일부다.
- Association: 단순 참조. 다른 BO나 마스터 데이터를 가리킬 뿐이다.
자식 뷰(ZR_BOOKING_JSC)에는 반드시 반대 방향의 association to parent 를 선언해야 한다.
abap
define view entity ZR_BOOKING_JSC
as select from zbooking_jsc as Booking
association to parent ZR_TRAVEL_JSC as _Travel
on $projection.TravelUUID = _Travel.TravelUUID
{
key booking_uuid as BookingUUID,
travel_uuid as TravelUUID,
...
_Travel
}
5. Step 3 — Behavior Definition (BDL) 작성
여기가 RAP의 심장이다. 루트 뷰에서 우클릭 → New Behavior Definition 을 선택하면 골격이 생성된다.
abap
managed implementation in class zbp_r_travel_jsc unique;
strict ( 2 );
with draft;
define behavior for ZR_TRAVEL_JSC alias Travel
persistent table ztravel_jsc
draft table ztravel_jsc_d
lock master
total etag LastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
{
field ( numbering : managed, readonly ) TravelUUID;
field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt,
LocalLastChangedAt, TotalPrice, OverallStatus;
field ( mandatory ) AgencyID, CustomerID, BeginDate, EndDate;
create;
update;
delete;
association _Booking { create ( features : instance ); with draft; }
// 결정(Determination)
determination setInitialStatus on modify { create; }
determination calculateTotalPrice on modify { field BookingFee, CurrencyCode; }
// 검증(Validation)
validation validateCustomer on save { create; field CustomerID; }
validation validateDates on save { create; field BeginDate, EndDate; }
// 액션(Action)
action ( features : instance ) acceptTravel result [1] $self;
action ( features : instance ) rejectTravel result [1] $self;
// Draft 액션
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare
{
validation validateCustomer;
validation validateDates;
}
mapping for ztravel_jsc
{
TravelUUID = travel_uuid;
TravelID = travel_id;
AgencyID = agency_id;
CustomerID = customer_id;
BeginDate = begin_date;
EndDate = end_date;
BookingFee = booking_fee;
TotalPrice = total_price;
CurrencyCode = currency_code;
Description = description;
OverallStatus = overall_status;
CreatedBy = created_by;
CreatedAt = created_at;
LastChangedBy = last_changed_by;
LastChangedAt = last_changed_at;
LocalLastChangedAt = local_last_changed_at;
}
}
주요 키워드 해설
| 키워드 | 의미 |
|---|---|
strict ( 2 ) | 최신 문법 검사 모드. 신규 개발은 반드시 켠다. 나중에 켜면 수정할 게 많아진다 |
with draft | Draft(임시 저장) 기능 활성화. Fiori에서 편집 중 이탈해도 내용이 보존된다 |
lock master | 이 엔티티가 잠금 주체. 자식은 lock dependent by _Travel |
numbering : managed | UUID 키를 프레임워크가 자동 생성 (키 타입이 RAW16일 때만) |
field ( features : instance ) | 인스턴스 상태에 따라 동적으로 활성/비활성 결정 |
mapping for | CDS 필드명 ↔ DB 컬럼명 매핑. 이름이 다르면 필수 |
Draft 테이블 생성
draft table ztravel_jsc_d 를 적으면 처음엔 에러가 난다. 에러 위에서 Quick Fix (Ctrl+1) → “Create draft table” 을 선택하면 필요한 관리 필드(draftuuid, draftentitycreationdatetime 등)를 포함한 테이블이 자동 생성된다. 손으로 만들지 말 것.
Determination / Validation / Action 구분
세 가지가 헷갈리기 쉬운데, 역할이 명확히 다르다.
- Determination: 값을 자동으로 채우는 로직. 예) 생성 시 상태를 ‘Open’으로 설정, 예약료 변경 시 총액 재계산
- Validation: 값을 검사하는 로직. 실패 시 저장을 막고 메시지를 반환. 데이터를 변경하면 안 된다
- Action: 사용자가 버튼을 눌러 실행하는 로직. Fiori 화면에 버튼으로 표시된다
6. Step 4 — Behavior Implementation 클래스 구현
BDL에서 implementation in class zbp_r_travel_jsc 부분에 커서를 두고 Quick Fix를 실행하면 클래스가 생성된다. 로컬 핸들러 클래스(lhc_Travel)에 메서드 골격까지 자동으로 만들어진다.
6-1. Determination 구현
abap
METHOD setInitialStatus.
" 1) 현재 값 읽기
READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
ENTITY Travel
FIELDS ( OverallStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(travels).
" 2) 이미 상태가 있는 건은 제외
DELETE travels WHERE OverallStatus IS NOT INITIAL.
CHECK travels IS NOT INITIAL.
" 3) 기본값 'O'(Open) 설정
MODIFY ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( OverallStatus )
WITH VALUE #( FOR travel IN travels
( %tky = travel-%tky
OverallStatus = 'O' ) )
REPORTED DATA(update_reported).
reported = CORRESPONDING #( DEEP update_reported ).
ENDMETHOD.
여기서 등장하는 READ ENTITIES / MODIFY ENTITIES 가 바로 EML(Entity Manipulation Language) 이다. RAP에서는 SELECT 나 UPDATE 로 DB에 직접 접근하지 않고, 반드시 EML로 트랜잭션 버퍼를 거친다.
IN LOCAL MODE를 빼먹지 말 것. 이 구문은 권한 체크와 피처 컨트롤을 건너뛰고 BO 내부에서 직접 접근한다는 뜻이다. 핸들러 클래스 내부에서는 거의 항상 필요하다.
6-2. Validation 구현
abap
METHOD validateCustomer.
READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
ENTITY Travel
FIELDS ( CustomerID )
WITH CORRESPONDING #( keys )
RESULT DATA(travels).
DATA customers TYPE SORTED TABLE OF /dmo/customer
WITH UNIQUE KEY customer_id.
customers = CORRESPONDING #( travels DISCARDING DUPLICATES
MAPPING customer_id = CustomerID EXCEPT * ).
DELETE customers WHERE customer_id IS INITIAL.
IF customers IS NOT INITIAL.
SELECT FROM /dmo/customer FIELDS customer_id
FOR ALL ENTRIES IN @customers
WHERE customer_id = @customers-customer_id
INTO TABLE @DATA(valid_customers).
ENDIF.
LOOP AT travels INTO DATA(travel).
" 상태 초기화 (이전 검증 메시지 제거)
APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = travel-%tky
%state_area = 'VALIDATE_CUSTOMER' )
TO reported-travel.
IF travel-CustomerID IS INITIAL.
APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = travel-%tky
%state_area = 'VALIDATE_CUSTOMER'
%msg = NEW /dmo/cm_flight_messages(
textid = /dmo/cm_flight_messages=>enter_customer_id
severity = if_abap_behv_message=>severity-error )
%element-CustomerID = if_abap_behv=>mk-on )
TO reported-travel.
ELSEIF NOT line_exists( valid_customers[ customer_id = travel-CustomerID ] ).
APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = travel-%tky
%state_area = 'VALIDATE_CUSTOMER'
%msg = NEW /dmo/cm_flight_messages(
textid = /dmo/cm_flight_messages=>customer_unkown
customer_id = travel-CustomerID
severity = if_abap_behv_message=>severity-error )
%element-CustomerID = if_abap_behv=>mk-on )
TO reported-travel.
ENDIF.
ENDLOOP.
ENDMETHOD.
핵심 패턴은 이렇다. 검증 실패 시 failed 테이블에 키를 넣어 저장을 막고, reported 테이블에 메시지를 넣어 사용자에게 알린다. %element-CustomerID 를 켜두면 Fiori 화면에서 해당 입력 필드가 빨갛게 표시된다.
6-3. Action 구현
abap
METHOD acceptTravel.
" 상태를 'A'(Accepted)로 변경
MODIFY ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( OverallStatus )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
OverallStatus = 'A' ) ).
" 변경된 인스턴스를 다시 읽어 result로 반환
READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
ENTITY Travel
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(travels).
result = VALUE #( FOR travel IN travels
( %tky = travel-%tky
%param = travel ) ).
ENDMETHOD.
6-4. 인스턴스 피처 컨트롤
이미 승인된 여행에 “승인” 버튼이 활성화되어 있으면 곤란하다. 상태에 따라 버튼을 비활성화한다.
abap
METHOD get_instance_features.
READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
ENTITY Travel
FIELDS ( TravelUUID OverallStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(travels)
FAILED failed.
result = VALUE #( FOR travel IN travels
( %tky = travel-%tky
%action-acceptTravel =
COND #( WHEN travel-OverallStatus = 'A'
THEN if_abap_behv=>fc-o-disabled
ELSE if_abap_behv=>fc-o-enabled )
%action-rejectTravel =
COND #( WHEN travel-OverallStatus = 'X'
THEN if_abap_behv=>fc-o-disabled
ELSE if_abap_behv=>fc-o-enabled ) ) ).
ENDMETHOD.
7. Step 5 — 프로젝션 레이어 (소비 계층)
여기서 많은 개발자가 “왜 뷰를 두 번 만들지?” 라고 묻는다. 답은 재사용성 이다.
ZR_(Interface/Base 레이어): 비즈니스 로직과 데이터 모델의 단일 원천. 여러 서비스가 공유ZC_(Consumption/Projection 레이어): 특정 UI나 API를 위한 맞춤 노출. 필요한 필드만 골라내고 UI 어노테이션을 붙임
같은 BO를 관리자용 앱과 조회 전용 앱에 각각 다르게 노출해야 할 때, 프로젝션 레이어만 하나 더 만들면 된다.
프로젝션 뷰
abap
@EndUserText.label: 'Travel - Consumption View'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZC_TRAVEL_JSC
provider contract transactional_query
as projection on ZR_TRAVEL_JSC
{
@Search.defaultSearchElement: true
key TravelUUID,
TravelID,
AgencyID,
CustomerID,
BeginDate,
EndDate,
BookingFee,
TotalPrice,
CurrencyCode,
Description,
OverallStatus,
LastChangedAt,
LocalLastChangedAt,
/* Associations */
_Booking : redirected to composition child ZC_BOOKING_JSC,
_Agency,
_Customer,
_Currency
}
provider contract transactional_query 와 redirected to 가 핵심이다. Composition은 반드시 프로젝션 계층의 자식으로 리디렉션해야 한다.
프로젝션 Behavior Definition
abap
projection;
strict ( 2 );
use draft;
define behavior for ZC_TRAVEL_JSC alias Travel
{
use create;
use update;
use delete;
use action acceptTravel;
use action rejectTravel;
use action Edit;
use action Activate;
use action Discard;
use action Resume;
use action Prepare;
use association _Booking { create; with draft; }
}
프로젝션 BDL에서는 기본 계층에 선언된 것 중 노출할 것만 골라서 use 한다. 조회 전용 앱이라면 use create/update/delete 를 빼면 그만이다.
8. Step 6 — Service Definition & Binding
Service Definition
abap
@EndUserText.label: 'Service Definition for Travel'
define service ZUI_TRAVEL_JSC
{
expose ZC_TRAVEL_JSC as Travel;
expose ZC_BOOKING_JSC as Booking;
expose /DMO/I_Agency as Agency;
expose /DMO/I_Customer as Customer;
expose I_Currency as Currency;
}
밸류 헬프(F4)에 쓰이는 마스터 데이터 뷰도 함께 노출해야 화면에서 정상 동작한다. 이걸 빼먹어 F4가 안 뜨는 경우가 많다.
Service Binding
Service Definition에서 우클릭 → New Service Binding.
| 바인딩 타입 | 용도 |
|---|---|
| OData V4 – UI | Fiori Elements 앱 (신규 개발 권장) |
| OData V2 – UI | 구버전 Fiori 호환 필요 시 |
| OData V4 – Web API | 시스템 간 연동 API |
생성 후 Activate 버튼을 누르면 서비스가 활성화되고, 엔티티를 선택한 뒤 Preview 를 누르면 브라우저에서 바로 Fiori Elements 화면이 뜬다. 별도 UI5 개발 없이 여기까지 온 것이다.
9. Step 7 — 메타데이터 확장으로 화면 다듬기
프리뷰를 처음 열면 컬럼이 하나도 안 보이거나 배치가 엉망일 것이다. UI 어노테이션이 없기 때문이다. 프로젝션 뷰에 직접 쓸 수도 있지만, @Metadata.allowExtensions: true 를 켜뒀으니 Metadata Extension 으로 분리하는 편이 깔끔하다.
abap
@Metadata.layer: #CORE
@UI: {
headerInfo: {
typeName: 'Travel',
typeNamePlural: 'Travels',
title: { type: #STANDARD, value: 'TravelID' }
}
}
annotate entity ZC_TRAVEL_JSC with
{
@UI.facet: [
{ id: 'Travel', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
label: 'Travel', position: 10 },
{ id: 'Booking', purpose: #STANDARD, type: #LINEITEM_REFERENCE,
label: 'Booking', position: 20, targetElement: '_Booking' }
]
@UI.hidden: true
TravelUUID;
@UI: {
lineItem: [ { position: 10 } ],
identification: [ { position: 10 } ],
selectionField: [ { position: 10 } ]
}
TravelID;
@UI: {
lineItem: [ { position: 20 } ],
identification: [ { position: 20 } ],
selectionField: [ { position: 20 } ]
}
@Consumption.valueHelpDefinition: [ { entity: { name: '/DMO/I_Agency',
element: 'AgencyID' } } ]
AgencyID;
@UI: {
lineItem: [ { position: 60, importance: #HIGH },
{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: '승인' },
{ type: #FOR_ACTION, dataAction: 'rejectTravel', label: '반려' } ]
}
OverallStatus;
}
@UI.facet— 상세 화면의 섹션 구성.#LINEITEM_REFERENCE로 자식 테이블을 붙인다@UI.lineItem— 목록(List Report) 컬럼@UI.selectionField— 상단 필터 필드@Consumption.valueHelpDefinition— F4 밸류 헬프type: #FOR_ACTION— 액션을 버튼으로 노출
10. 자주 만나는 오류와 해결법
“Entity does not have an ETag field” BDL에 etag master LocalLastChangedAt 선언이 빠졌거나, CDS 뷰에 @Semantics.systemDateTime.localInstanceLastChangedAt: true 어노테이션이 없다.
“Field … is not mapped” CDS 필드명과 DB 컬럼명이 다른데 mapping for 블록에 해당 필드가 없다. Quick Fix로 매핑 전체를 생성할 수 있다.
Determination에서 값을 바꿨는데 저장이 안 됨 MODIFY ENTITIES 에 IN LOCAL MODE 를 빠뜨렸거나, 해당 필드가 field ( readonly ) 로 선언되어 있는 경우다. readonly 필드는 프레임워크가 아니라 determination에서만 변경 가능하도록 별도 처리가 필요하다.
%tky vs %key Draft를 쓴다면 무조건 %tky(transactional key)를 쓴다. %tky 는 키에 더해 Draft 여부(%is_draft)까지 포함한다. %key 만 쓰면 Draft 인스턴스를 못 찾는다.
Fiori 프리뷰에서 “승인” 버튼이 안 보임 프로젝션 BDL에 use action acceptTravel; 을 선언했는지, 메타데이터 확장에 type: #FOR_ACTION 을 넣었는지 확인한다. 둘 다 필요하다.
UUID가 자동 생성되지 않음 numbering : managed 는 키 필드 타입이 sysuuid_x16(RAW16)일 때만 동작한다. CHAR 타입 키라면 early numbering 을 직접 구현해야 한다.
11. 끝으로
RAP으로 BO 하나를 만들면서 얻는 가장 큰 소득은 관심사의 분리 다.
- 데이터 모델은 CDS가
- 트랜잭션 동작은 BDL이
- 비즈니스 로직은 핸들러 클래스가
- 화면은 어노테이션이
각각 책임진다. 클래식 ABAP에서 다이나믹 프로그램 하나에 화면 로직과 DB 접근과 검증이 뒤엉켜 있던 것과 비교하면, 유지보수 관점에서 차원이 다르다.
처음에는 만들어야 할 오브젝트가 많아 부담스럽게 느껴지지만, 두세 번 반복하면 패턴이 몸에 익는다. ADT의 Quick Fix가 골격을 대부분 만들어주기 때문에 실제로 타이핑하는 양은 생각보다 적다.
다음 단계로 학습을 이어간다면 이 순서를 권한다.
- Draft 처리 심화 — Draft 인스턴스와 Active 인스턴스의 라이프사이클
- 비즈니스 이벤트(RAP Business Events) — 상태 변경 시 이벤트 발행
- 권한 제어 — CDS Access Control(DCL) +
authorization master - Unmanaged 시나리오 — 레거시 함수모듈을 RAP으로 래핑하기
- Side Effects 어노테이션 — 필드 변경 시 다른 필드를 즉시 갱신
SAP에서 제공하는 openSAP 강의와 RAP 개발 가이드(공식 문서), 그리고 /DMO/ 데모 패키지의 소스 코드가 가장 좋은 교재다. 특히 /DMO/ 패키지는 Managed / Unmanaged / Draft 시나리오별 완성된 예제가 모두 들어 있으니, 막힐 때마다 열어보길 권한다.
