[RAP] Managed 시나리오 심화 — Validation, Determination, Action 완전 구현 가이드

Moderator
RAP 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에서 실제 로직을 작성하는 전 과정을 다룹니다.

2. 선수 지식

RAP 4-Layer 구조가 낯선 경우, 입문편("[RAP] ABAP RESTful Application Programming Model 입문 — Business Object 4-Layer 구조 완전 가이드")을 먼저 참고하시기 바랍니다.

3. 환경 / 버전 / 준비물

항목권장 사양
SAP BTP ABAP Environment2022 이상 (또는 S/4HANA 2021 이상)
ADT (Eclipse)최신 버전 권장
ABAP Language VersionABAP 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이 실패하면 저장이 거부되고, failedreported 구조체를 통해 오류 메시지가 사용자에게 전달됩니다.

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;
  }
}

주요 문법 포인트를 정리합니다.

6. Validation 구현

validateCustomer — 고객 존재 여부 검증

Validation 메서드의 기본 패턴은 다음과 같습니다: (1) READ ENTITIES로 현재 BO 데이터를 읽고, (2) 비즈니스 규칙을 검증한 후, (3) 실패 시 failedreported에 오류를 추가합니다.

  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의 동작 원리

BDEF에서의 선언을 다시 보면:

  draft determine action Prepare
  {
    validation validateCustomer;
    validation validateDates;
  }

이 선언만으로 Draft-Active 전환 시 자동으로 양쪽 Validation이 호출됩니다. 별도의 Prepare 메서드 구현은 필요하지 않으며, RAP 프레임워크가 내부적으로 처리합니다.

Draft 표준 액션 정리

Draft Action역할
EditActive 인스턴스를 Draft로 전환 (편집 시작)
Activate optimizedDraft를 Active로 전환 (변경분만 저장)
DiscardDraft를 삭제 (편집 취소)
Resume이전에 중단된 Draft 편집을 재개
PrepareActivate 전 Validation 사전 실행

10. 흔한 실수 / 트러블슈팅

Q1. Validation이 실행되지 않는 것 같습니다.

BDEF에서 field 키워드로 지정한 필드가 실제로 변경되었는지 확인하시기 바랍니다. 예를 들어 validateDatesfield 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. 다음 단계 / 관련 주제

12. 참고 자료