[RAP] Managed 시나리오 심화 — Validation, Determination, Action 완전 구현 가이드
1. 개요 및 학습 목표
SAP RAP(RESTful Application Programming Model)의 Managed 시나리오에서 Business Object의 실질적인 비즈니스 로직은 Validation, Determination, Action 세 가지 메커니즘으로 구현됩니다. 이 튜토리얼은 RAP의 4-Layer 구조(입문편에서 다룬 내용)를 이미 이해한 개발자를 대상으로, Behavior Definition(BDEF)에서 이 세 가지를 선언하고 Behavior Implementation Class에서 실제 로직을 작성하는 전 과정을 다룹니다.
- Validation으로 저장 시점 데이터 검증 로직을 구현할 수 있다
- Determination으로 레코드 생성/수정 시 자동 필드 설정을 구현할 수 있다
- Instance Action과 Factory Action의 차이를 이해하고 각각 구현할 수 있다
- Draft 시나리오에서 Prepare Action과 Validation의 연동 구조를 설명할 수 있다
- failed/reported 구조체를 활용한 오류 처리 패턴을 적용할 수 있다
2. 선수 지식
- ABAP 기본 문법 (클래스, 메서드, 인터널 테이블)
- RAP Business Object의 4-Layer 구조 이해 (Data Model → Service Definition → Service Binding → Behavior Definition)
- CDS View Entity 기본 개념
- ADT(ABAP Development Tools) 사용 경험
RAP 4-Layer 구조가 낯선 경우, 입문편("[RAP] ABAP RESTful Application Programming Model 입문 — Business Object 4-Layer 구조 완전 가이드")을 먼저 참고하시기 바랍니다.
3. 환경 / 버전 / 준비물
| 항목 | 권장 사양 |
|---|---|
| SAP BTP ABAP Environment | 2022 이상 (또는 S/4HANA 2021 이상) |
| ADT (Eclipse) | 최신 버전 권장 |
| ABAP Language Version | ABAP for Cloud Development |
| 참고 워크숍 | SAP RAP100 (openSAP / SAP-samples) |
이 튜토리얼의 코드 예시는 SAP RAP100 워크숍의 Travel 시나리오를 기반으로 합니다. Persistent table은 zrap100_atravsol, CDS Entity는 ZRAP100_R_TravelTP_SOL 형태를 가정합니다. 실습 환경에 따라 접미사(SOL 등)가 달라질 수 있으므로 본인의 네이밍 규칙에 맞게 치환하시기 바랍니다.
4. 핵심 개념 — Validation, Determination, Action의 역할 분담
Managed 시나리오에서 RAP 런타임이 CRUD를 자동 처리해주지만, 비즈니스 로직은 개발자가 직접 작성해야 합니다. 이때 세 가지 메커니즘이 각각 다른 시점과 목적으로 동작합니다.
Validation (검증)
데이터가 저장(save)되기 직전에 실행되어 비즈니스 규칙을 검증합니다. 예를 들어 "CustomerID가 실제 존재하는 고객인가?", "시작일이 종료일보다 앞서는가?" 같은 검증입니다. 비유하자면 은행 창구에서 서류를 제출할 때 직원이 최종 확인하는 단계에 해당합니다. Validation이 실패하면 저장이 거부되고, failed와 reported 구조체를 통해 오류 메시지가 사용자에게 전달됩니다.
Determination (자동 결정)
레코드가 생성(create) 또는 수정(update)될 때 특정 필드를 자동으로 설정합니다. 예를 들어 "Travel을 새로 만들면 상태를 자동으로 Open으로 설정"하는 것입니다. 비유하자면 새 주문서를 작성하면 시스템이 자동으로 "접수" 도장을 찍어주는 것과 같습니다. on modify 시점에 실행되며, MODIFY ENTITIES IN LOCAL MODE를 사용해 BO 내부에서 데이터를 변경합니다.
Action (사용자 액션)
사용자가 UI 버튼을 눌러 명시적으로 트리거하는 비즈니스 오퍼레이션입니다. "여행 승인", "여행 거부", "할인 적용" 같은 동작이 해당됩니다. Instance Action은 기존 인스턴스를 변경하고, Factory Action은 새 인스턴스를 생성합니다. 비유하자면 워크플로우에서 "승인" 버튼을 클릭하는 행위 자체입니다.
실행 시점 요약: Determination은 데이터 변경 직후(on modify), Validation은 저장 직전(on save), Action은 사용자 요청 시점에 각각 실행됩니다. 이 순서를 이해하는 것이 RAP 비즈니스 로직 설계의 핵심입니다.
5. BDEF(Behavior Definition) 선언
모든 Validation, Determination, Action은 Behavior Definition 파일에 먼저 선언해야 합니다. 아래는 Travel BO의 전체 BDEF 예시입니다.
managed implementation in class zbp_rap100_r_traveltp_sol unique;
strict ( 2 );
with draft;
define behavior for ZRAP100_R_TravelTP_SOL alias Travel
persistent table zrap100_atravsol
draft table zrap100_dtrvlsol
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master ( global )
early numbering
{
field ( readonly )
CreatedAt, CreatedBy, LastChangedAt, LastChangedBy, LocalLastChangedAt;
field ( readonly ) TravelID;
field ( mandatory ) CustomerID, BeginDate, EndDate;
create;
update;
delete;
// --- Validations ---
validation validateCustomer on save { create; field CustomerID; }
validation validateDates on save { create; update; field BeginDate, EndDate; }
// --- Determinations ---
determination setStatusToOpen on modify { create; }
// --- Instance Actions ---
action deductDiscount parameter zrap100_a_trvlparasol result [1] $self;
action acceptTravel result [1] $self;
action rejectTravel result [1] $self;
// --- Factory Action ---
factory action copyTravel [1];
// --- Draft Actions (표준) ---
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare
{
validation validateCustomer;
validation validateDates;
}
}
주요 문법 포인트를 정리합니다.
validation ... on save { create; field ...; }— 저장 시점에 실행되며,create나update트리거와 감시 대상 필드를 지정합니다.determination ... on modify { create; }— 수정 시점(레코드 생성 포함)에 실행됩니다.action ... parameter ... result [1] $self;— 입력 파라미터가 있는 인스턴스 액션입니다.$self는 동일 엔티티를 결과로 반환함을 의미합니다.factory action ... [1];— 새 인스턴스를 생성하는 팩토리 액션입니다.draft determine action Prepare— Draft 저장(Prepare) 시 지정된 Validation들을 미리 실행하여 사용자에게 피드백을 줍니다.
6. Validation 구현
validateCustomer — 고객 존재 여부 검증
Validation 메서드의 기본 패턴은 다음과 같습니다: (1) READ ENTITIES로 현재 BO 데이터를 읽고, (2) 비즈니스 규칙을 검증한 후, (3) 실패 시 failed와 reported에 오류를 추가합니다.
METHOD validateCustomer.
" 1. 현재 Travel 엔티티에서 CustomerID 읽기
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
FIELDS ( CustomerID )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
" 2. 유효한 Customer 목록 조회
DATA lt_customers TYPE SORTED TABLE OF /dmo/customer
WITH UNIQUE KEY customer_id.
SELECT FROM /dmo/customer
FIELDS customer_id
FOR ALL ENTRIES IN @lt_travels
WHERE customer_id = @lt_travels-CustomerID
INTO TABLE @lt_customers.
" 3. 검증 실패 시 failed/reported 채우기
LOOP AT lt_travels INTO DATA(ls_travel).
IF ls_travel-CustomerID IS INITIAL
OR NOT line_exists( lt_customers[ customer_id = ls_travel-CustomerID ] ).
APPEND VALUE #( %tky = ls_travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = ls_travel-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Customer { ls_travel-CustomerID } is not valid.| )
%element-CustomerID = if_abap_behv=>mk-on
) TO reported-travel.
ENDIF.
ENDLOOP.
ENDMETHOD.
validateDates — 날짜 유효성 검증
METHOD validateDates.
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
FIELDS ( BeginDate EndDate TravelID )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
LOOP AT lt_travels INTO DATA(ls_travel).
" 시작일이 오늘 이전인 경우
IF ls_travel-BeginDate < cl_abap_context_info=>get_system_date( ).
APPEND VALUE #( %tky = ls_travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = ls_travel-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Begin date must be in the future.| )
%element-BeginDate = if_abap_behv=>mk-on
) TO reported-travel.
ENDIF.
" 종료일이 시작일보다 앞서는 경우
IF ls_travel-EndDate < ls_travel-BeginDate.
APPEND VALUE #( %tky = ls_travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = ls_travel-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |End date must be after begin date.| )
%element-EndDate = if_abap_behv=>mk-on
) TO reported-travel.
ENDIF.
ENDLOOP.
ENDMETHOD.
핵심 포인트: %element 필드를 if_abap_behv=>mk-on으로 설정하면, Fiori Elements UI에서 해당 필드에 빨간색 오류 표시가 나타납니다. 이것이 RAP의 필드 수준 오류 바인딩 메커니즘입니다.
7. Determination 구현
METHOD setStatusToOpen.
" 새로 생성된 Travel의 상태를 자동으로 'O'(Open)으로 설정
MODIFY ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
UPDATE
FIELDS ( OverallStatus )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
OverallStatus = 'O' ) ).
ENDMETHOD.
Determination에서 주목할 점은 MODIFY ENTITIES IN LOCAL MODE를 사용한다는 것입니다. LOCAL MODE는 권한 검사(authorization check)와 기타 Validation을 건너뛰고 BO 내부에서 직접 데이터를 수정합니다. Determination은 BO 자체의 내부 로직이므로 이 모드가 적절합니다.
또한 Determination은 failed/reported를 반환하지 않는 것이 일반적입니다. 자동 설정 로직이므로 실패할 상황이 거의 없기 때문입니다. 만약 외부 데이터 의존성이 있다면 Validation에서 별도로 검증하는 것이 권장되는 패턴입니다.
8. Action 구현 — Instance Action과 Factory Action
Instance Action: acceptTravel / rejectTravel
상태를 변경하는 단순한 Instance Action 패턴입니다.
METHOD acceptTravel.
" 1. 현재 상태 읽기
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
FIELDS ( OverallStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
" 2. 이미 승인/거부된 건 필터링
LOOP AT lt_travels INTO DATA(ls_travel)
WHERE OverallStatus = 'O'. " Open 상태만 처리
" 3. 상태를 Accepted로 변경
MODIFY ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
UPDATE
FIELDS ( OverallStatus )
WITH VALUE #( ( %tky = ls_travel-%tky
OverallStatus = 'A' ) ).
ENDLOOP.
" 4. 결과 반환 (result 파라미터)
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #( FOR travel IN lt_result
( %tky = travel-%tky
%param = travel ) ).
ENDMETHOD.
Instance Action with Parameter: deductDiscount
deductDiscount는 사용자가 할인율을 입력받아 총 가격에서 차감하는 액션입니다. parameter 키워드로 선언한 Abstract Entity가 입력 구조체로 사용됩니다.
METHOD deductDiscount.
DATA lt_update TYPE TABLE FOR UPDATE ZRAP100_R_TravelTP_SOL\\Travel.
" 1. 현재 가격 읽기
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
FIELDS ( TotalPrice BookingFee )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
" 2. 할인 계산 및 업데이트 준비
LOOP AT lt_travels INTO DATA(ls_travel).
DATA(lv_discount_pct) = keys[ KEY entity %tky = ls_travel-%tky ]-%param-discount_pct.
IF lv_discount_pct > 0 AND lv_discount_pct <= 100.
DATA(lv_new_price) = ls_travel-TotalPrice * ( 1 - lv_discount_pct / 100 ).
APPEND VALUE #( %tky = ls_travel-%tky
TotalPrice = lv_new_price )
TO lt_update.
ENDIF.
ENDLOOP.
" 3. 가격 업데이트
MODIFY ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( TotalPrice )
WITH lt_update.
" 4. 결과 반환
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #( FOR travel IN lt_result
( %tky = travel-%tky
%param = travel ) ).
ENDMETHOD.
Factory Action: copyTravel
Factory Action은 기존 인스턴스를 기반으로 새 인스턴스를 생성합니다. 기존 Action과 달리 MODIFY ENTITIES ... CREATE를 사용하며, mapped 파라미터를 통해 새로 생성된 키를 반환합니다.
METHOD copyTravel.
" 1. 원본 Travel 읽기
READ ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
LOOP AT lt_travels INTO DATA(ls_travel).
" 2. 새 Travel 데이터 준비 (키와 관리 필드 초기화)
DATA(ls_new) = ls_travel.
ls_new-TravelID = ''. " Early Numbering이 새 ID 부여
ls_new-OverallStatus = 'O'. " 상태를 Open으로 리셋
ls_new-BeginDate = cl_abap_context_info=>get_system_date( ).
ls_new-EndDate = cl_abap_context_info=>get_system_date( ) + 14.
" 3. 새 인스턴스 생성
MODIFY ENTITIES OF ZRAP100_R_TravelTP_SOL IN LOCAL MODE
ENTITY Travel
CREATE
FIELDS ( AgencyID CustomerID BeginDate EndDate
BookingFee TotalPrice CurrencyCode
Description OverallStatus )
WITH VALUE #( ( %cid = keys[ KEY entity %tky = ls_travel-%tky ]-%cid
AgencyID = ls_new-AgencyID
CustomerID = ls_new-CustomerID
BeginDate = ls_new-BeginDate
EndDate = ls_new-EndDate
BookingFee = ls_new-BookingFee
TotalPrice = ls_new-TotalPrice
CurrencyCode = ls_new-CurrencyCode
Description = |Copy of { ls_travel-Description }|
OverallStatus = ls_new-OverallStatus ) )
MAPPED DATA(ls_mapped).
" 4. mapped 반환
mapped-travel = ls_mapped-travel.
ENDLOOP.
ENDMETHOD.
Factory Action의 핵심은 %cid(Content ID)를 사용하여 아직 키가 부여되지 않은 새 인스턴스를 식별한다는 점입니다. Early Numbering 설정이 되어 있으면 RAP 프레임워크가 자동으로 새 TravelID를 할당합니다.
9. Draft와 Prepare Action 연동
Draft 시나리오에서는 사용자가 데이터를 입력하는 동안 아직 Active 테이블에 저장되지 않은 Draft 상태로 유지됩니다. 이때 Prepare Action이 중요한 역할을 합니다.
Prepare의 동작 원리
- 사용자가 Fiori UI에서 저장 버튼을 누르면, 먼저
Prepare가 실행됩니다. - Prepare는 BDEF에서 지정한 Validation들(
validateCustomer,validateDates)을 Draft 데이터에 대해 실행합니다. - Validation이 통과하면
Activate가 진행되어 Draft가 Active 테이블로 이동합니다. - Validation이 실패하면 사용자에게 오류 메시지가 표시되고, Draft 상태가 유지됩니다.
BDEF에서의 선언을 다시 보면:
draft determine action Prepare
{
validation validateCustomer;
validation validateDates;
}
이 선언만으로 Draft-Active 전환 시 자동으로 양쪽 Validation이 호출됩니다. 별도의 Prepare 메서드 구현은 필요하지 않으며, RAP 프레임워크가 내부적으로 처리합니다.
Draft 표준 액션 정리
| Draft Action | 역할 |
|---|---|
Edit | Active 인스턴스를 Draft로 전환 (편집 시작) |
Activate optimized | Draft를 Active로 전환 (변경분만 저장) |
Discard | Draft를 삭제 (편집 취소) |
Resume | 이전에 중단된 Draft 편집을 재개 |
Prepare | Activate 전 Validation 사전 실행 |
10. 흔한 실수 / 트러블슈팅
Q1. Validation이 실행되지 않는 것 같습니다.
BDEF에서 field 키워드로 지정한 필드가 실제로 변경되었는지 확인하시기 바랍니다. 예를 들어 validateDates는 field BeginDate, EndDate로 선언되어 있으므로, 이 필드가 변경되지 않으면 트리거되지 않습니다. 또한 create/update 트리거 조건도 확인이 필요합니다.
Q2. Determination에서 MODIFY ENTITIES 호출 시 authorization 오류가 발생합니다.
IN LOCAL MODE를 빠뜨렸을 가능성이 높습니다. Determination은 BO 내부 로직이므로 반드시 MODIFY ENTITIES OF ... IN LOCAL MODE를 사용해야 합니다. LOCAL MODE 없이 호출하면 권한 검사가 다시 실행되어 무한 루프나 권한 오류가 발생할 수 있습니다.
Q3. Factory Action 실행 후 새 레코드의 키 값이 비어 있습니다.
Early Numbering이 설정되어 있는지 확인하시기 바랍니다. BDEF에 early numbering이 선언되어 있으면 earlynumbering_create 메서드에서 키 할당 로직이 구현되어야 합니다. Late Numbering을 사용하는 경우 adjust_numbers 메서드에서 처리됩니다.
Q4. %tky와 %key의 차이가 무엇인가요?
%key는 BO의 비즈니스 키(예: TravelID)만 포함하고, %tky(transactional key)는 Draft 시나리오에서 %is_draft 플래그까지 포함합니다. Draft를 사용하는 BO에서는 항상 %tky를 사용하는 것이 권장됩니다.
11. 다음 단계 / 관련 주제
- Dynamic Feature Control — 상태에 따라 Action 버튼을 활성화/비활성화하는 방법 (예: 이미 승인된 Travel에서 acceptTravel 버튼 숨기기)
- Side Effects — 특정 필드 변경 시 UI를 자동 새로고침하여 Determination 결과를 즉시 반영
- Authorization Control — instance/global 권한 검사 구현
- Unmanaged Save — Managed 시나리오에서 저장 로직을 커스터마이징해야 하는 경우
- Composition (하위 엔티티) — Travel-Booking 같은 부모-자식 구조에서 Validation/Determination 전파