RAP

아직도 %control 없이 수정 처리해요? 3가지 #shorts #SAP #RAP

▶ YouTube에서 보기

1. %control이란 무엇인가 — RAP의 부분 업데이트 철학

SAP RAP(RESTful ABAP Programming Model)은 OData v4 기반의 트랜잭션 처리를 위해 설계되었습니다. OData v4의 PATCH 요청은 PUT과 달리 "리소스의 일부만 갱신한다"는 의미를 가지며, 클라이언트는 변경된 필드만 페이로드에 담아 서버로 전송할 수 있습니다. RAP은 이 부분 업데이트(partial update) 시맨틱을 ABAP 핸들러 레이어까지 그대로 전달해야 하므로, "어떤 필드가 실제로 변경됐는지" 알려주는 메타 정보가 필요합니다. 그 역할을 하는 것이 바로 %control 구조체입니다.

%control은 엔티티 키와 동일한 구조를 가지면서 각 필드의 타입이 abap_bool(정확히는 1바이트 플래그)로 정의된 동반(companion) 구조체입니다. 클라이언트가 보낸 페이로드의 필드가 %control-field_name = if_abap_behv=>mk-on으로 표시되어 있다면 "이 필드는 명시적으로 변경되었다"는 신호이고, mk-off라면 "건드리지 마라"는 신호입니다. 이 구분이 없다면 단순히 VALUE #( )로 초기화된 필드가 "비어 있는 값을 의도한 것인지" 아니면 "그냥 페이로드에 없었던 것인지" 구별할 수 없게 됩니다.

이 글에서는 PurchaseOrder 엔티티를 중심으로 Modify 핸들러(UPDATE) 안에서 %control을 읽어 변경된 필드만 선별 처리하는 방법, 그리고 검증 로직·로깅·사이드 이펙트 트리거에 이르기까지 실무에서 마주치는 패턴들을 단계별로 다룹니다.

2. %control 구조체 내부 동작 원리

OData 게이트웨이가 PATCH 요청을 수신하면 RAP 런타임은 페이로드의 JSON 키를 분석하여 "어떤 필드가 명시적으로 전송됐는지"를 표시합니다. 이때 내부적으로 만들어지는 derived type은 대략 다음과 같은 형태를 띱니다.

" RAP 런타임이 자동 생성하는 derived type 개념도
TYPES: BEGIN OF ts_purchaseorder_control,
         order_id        TYPE abap_bool,
         supplier_id     TYPE abap_bool,
         order_date      TYPE abap_bool,
         total_amount    TYPE abap_bool,
         currency_code   TYPE abap_bool,
         status          TYPE abap_bool,
       END OF ts_purchaseorder_control.

" 핸들러 내부에서는 다음과 같이 접근
DATA(lv_amount_changed) = entity-%control-total_amount.
" lv_amount_changed = if_abap_behv=>mk-on  → 변경됨
" lv_amount_changed = if_abap_behv=>mk-off → 변경되지 않음

if_abap_behv=>mk-on은 hex 01, mk-off는 hex 00 값을 가지며 abap_bool과 호환됩니다. 다만 가독성을 위해 항상 상수를 사용하는 것이 권장됩니다. 이는 향후 RAP 런타임이 "부분적으로 처리 중"이라는 제3의 상태를 도입할 가능성에 대비한 방어적 코딩이기도 합니다.

RAP Managed 시나리오에서는 프레임워크가 기본적인 필드 매핑을 담당하지만, 커스텀 비즈니스 로직이 필요한 경우 FOR MODIFY 핸들러를 직접 구현해야 합니다. 이때 %control의 정확한 이해가 필수입니다. Unmanaged 시나리오에서는 모든 CRUD 로직을 직접 구현하므로 %control의 역할이 더욱 중요해집니다. 특히 여러 필드가 동시에 변경되는 복합 업데이트 케이스나, 특정 필드 조합에만 반응해야 하는 비즈니스 규칙이 있을 때 %control 없이는 정확한 처리가 불가능합니다.

3. Modify 핸들러에서 %control 읽기 — 기본 패턴

아래는 PurchaseOrder 헤더의 UPDATE를 처리하는 Modify 핸들러의 가장 기본적인 형태입니다. 단일 필드 변경을 감지하는 패턴부터 시작합니다.

CLASS lhc_purchaseorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS update_purchaseorder FOR MODIFY
      IMPORTING entities FOR UPDATE PurchaseOrder.
ENDCLASS.

CLASS lhc_purchaseorder IMPLEMENTATION.
  METHOD update_purchaseorder.

    LOOP AT entities ASSIGNING FIELD-SYMBOL(<entity>).

      " total_amount가 변경됐을 때만 환율 재계산
      IF <entity>-%control-total_amount = if_abap_behv=>mk-on.
        DATA(lv_recalculated) = recalc_local_currency(
            iv_amount   = <entity>-total_amount
            iv_currency = <entity>-currency_code ).

        MODIFY ENTITIES OF ZI_PurchaseOrder
          ENTITY PurchaseOrder
            UPDATE FIELDS ( local_amount )
            WITH VALUE #( ( %tky          = <entity>-%tky
                            local_amount  = lv_recalculated ) )
          REPORTED DATA(ls_reported).
      ENDIF.

    ENDLOOP.

  ENDMETHOD.
ENDCLASS.

핵심은 %control-total_amountmk-on일 때만 환율 재계산 로직을 수행한다는 점입니다. 만약 이 체크 없이 recalc_local_currency를 항상 호출한다면, 클라이언트가 status만 변경한 요청에도 불필요한 계산이 발생하고, 더 나쁘게는 빈 값을 가진 total_amount로 잘못된 결과를 만들 수 있습니다. %control 체크는 성능 최적화이자 데이터 무결성 보호의 핵심 장치입니다.

4. 실전 예제: SalesOrder 금액 변경 감지

PurchaseOrder보다 더 복잡한 시나리오로 SalesOrder를 살펴봅니다. 금액 변경이 발생했을 때 승인 워크플로를 트리거하고 감사 로그를 남기는 패턴입니다.

METHOD update_salesorder.

  DATA: lt_log_entry TYPE STANDARD TABLE OF zif_so_audit_log=>ty_entry.

  LOOP AT entities ASSIGNING FIELD-SYMBOL(<entity>).

    DATA(ls_ctrl) = <entity>-%control.

    " (a) 공급업체 변경은 승인 워크플로 트리거
    IF ls_ctrl-customer_id = if_abap_behv=>mk-on.
      TRY.
          zcl_so_approval=>request_resubmission(
            iv_order_id     = <entity>-order_id
            iv_new_customer = <entity>-customer_id ).
        CATCH zcx_so_approval INTO DATA(lx_appr).
          APPEND VALUE #(
            %tky    = <entity>-%tky
            %msg    = new_message_with_text(
                       severity = if_abap_behv_message=>severity-error
                       text     = lx_appr->get_text( ) )
            %element-customer_id = if_abap_behv=>mk-on
          ) TO reported-salesorder.
          CONTINUE.
      ENDTRY.
    ENDIF.

    " (b) 금액 변경은 임계치 초과 시 감사 로그 기록
    IF ls_ctrl-net_amount = if_abap_behv=>mk-on AND
       <entity>-net_amount > 500000.
      APPEND VALUE #(
        order_id   = <entity>-order_id
        field_id   = 'NET_AMOUNT'
        new_value  = |{ <entity>-net_amount }|
        changed_by = cl_abap_context_info=>get_user_technical_name( )
        changed_at = cl_abap_context_info=>get_system_date( )
      ) TO lt_log_entry.
    ENDIF.

    " (c) 상태가 'CONFIRMED'로 변경되면 재고 예약 트리거
    IF ls_ctrl-order_status = if_abap_behv=>mk-on AND
       <entity>-order_status = 'CONFIRMED'.
      reserve_inventory_for_order( iv_order_id = <entity>-order_id ).
    ENDIF.

  ENDLOOP.

  IF lt_log_entry IS NOT INITIAL.
    zcl_so_audit_log=>write( lt_log_entry ).
  ENDIF.

ENDMETHOD.

이 예제의 핵심은 ls_ctrl이라는 로컬 변수에 %control을 복사하여 매번 <entity>-%control-xxx를 참조하는 대신 짧고 읽기 쉬운 코드를 만든다는 점입니다. 또한 각 분기가 독립적으로 동작하도록 구성되어, 고객 변경 처리 실패가 금액 로깅을 막지 않습니다. CONTINUE는 치명적 오류(승인 요청 실패)가 발생한 엔티티의 후속 처리를 건너뛰기 위해서만 사용합니다.

5. 여러 필드 동시 변경 감지 및 분기 처리

실무에서는 여러 필드가 동시에 변경되는 케이스가 빈번합니다. 이때 단순한 IF 체인보다 변경 필드 집합을 먼저 수집한 뒤 한 번에 처리하는 패턴이 더 유지보수하기 좋습니다.

METHOD update_productcatalog.

  DATA(lt_changed_fields) = collect_changed_fields( entities ).

  " 변경 필드 집합 기반으로 검증 수행
  validate_field_combinations(
    EXPORTING it_entities = entities
              it_fields   = lt_changed_fields
    CHANGING  cs_reported = reported
              cs_failed   = failed ).

  " 변경 필드에 따라 사이드 이펙트 실행
  trigger_side_effects( it_fields = lt_changed_fields ).

ENDMETHOD.

METHOD collect_changed_fields.
  LOOP AT it_entities ASSIGNING FIELD-SYMBOL(<e>).
    DATA(ls_row) = VALUE ty_changed_row( product_id = <e>-product_id ).

    IF <e>-%control-base_price    = if_abap_behv=>mk-on.
      INSERT 'BASE_PRICE'    INTO TABLE ls_row-fields.
    ENDIF.
    IF <e>-%control-discount_rate = if_abap_behv=>mk-on.
      INSERT 'DISCOUNT_RATE' INTO TABLE ls_row-fields.
    ENDIF.
    IF <e>-%control-valid_from    = if_abap_behv=>mk-on.
      INSERT 'VALID_FROM'    INTO TABLE ls_row-fields.
    ENDIF.
    IF <e>-%control-valid_to      = if_abap_behv=>mk-on.
      INSERT 'VALID_TO'      INTO TABLE ls_row-fields.
    ENDIF.

    APPEND ls_row TO rt_result.
  ENDLOOP.
ENDMETHOD.

이 패턴에서 validate_field_combinations는 예를 들어 "base_price와 discount_rate가 동시에 변경된 경우에는 반드시 valid_from도 설정되어야 한다"는 복합 조건을 검사합니다. 단순한 필드별 IF 체인으로는 이런 조합 규칙을 깔끔하게 표현하기 어렵습니다. 변경 필드를 SET으로 수집한 뒤 SET 연산(교집합, 포함 여부)으로 비즈니스 규칙을 적용하는 방식이 확장성 측면에서 훨씬 우수합니다.

6. %control과 %data를 함께 활용하는 패턴

RAP 핸들러의 importing 구조에는 %control뿐 아니라 직접 매핑된 필드(%data 또는 엔티티 필드)도 함께 제공됩니다. %control이 "변경되었는가"를 알려준다면, 실제 필드 값은 "변경된 새 값이 무엇인가"를 보관합니다. 둘을 결합하면 "변경된 필드 + 새 값"을 정확하게 추출할 수 있습니다.

DATA: lt_audit TYPE TABLE OF zaudit_product.

LOOP AT entities ASSIGNING FIELD-SYMBOL(<e>).

  " 변경된 컬럼만 audit 테이블에 저장
  IF <e>-%control-base_price = if_abap_behv=>mk-on.
    APPEND VALUE #( product_id = <e>-product_id
                    field_id   = 'BASE_PRICE'
                    new_value  = |{ <e>-base_price CURRENCY = <e>-currency_code }|
                    changed_by = cl_abap_context_info=>get_user_technical_name( )
                    changed_at = utclong_current( ) ) TO lt_audit.
  ENDIF.

  IF <e>-%control-valid_from = if_abap_behv=>mk-on.
    APPEND VALUE #( product_id = <e>-product_id
                    field_id   = 'VALID_FROM'
                    new_value  = |{ <e>-valid_from DATE = ISO }|
                    changed_by = cl_abap_context_info=>get_user_technical_name( )
                    changed_at = utclong_current( ) ) TO lt_audit.
  ENDIF.

ENDLOOP.

INSERT zaudit_product FROM TABLE @lt_audit.

이 패턴은 변경 이력(change history) 기능을 직접 구현해야 하는 시나리오에 유용합니다. RAP에는 표준 Change Document 기능과의 통합 옵션도 있지만, 커스텀 감사 로그 테이블을 사용하는 프로젝트에서는 위와 같이 %control 기반으로 직접 작성하는 것이 일반적입니다. utclong_current( )를 사용하여 UTC 기준 타임스탬프를 기록하면 글로벌 시스템에서도 일관성을 유지할 수 있습니다.

7. 주의해야 할 엣지케이스와 흔한 실수

실수 1: %control 체크 없이 모든 필드를 처리하기

겉으로는 동작하는 것처럼 보일 수 있지만, 미세한 버그가 누적됩니다. 클라이언트가 PATCH로 order_status만 보냈는데 서버가 net_amount = 0(초기값)으로 덮어쓰는 사고는 RAP 초보자가 가장 자주 겪는 문제입니다. %controlmk-off인 필드는 절대 비즈니스 로직의 입력으로 사용하지 않는 것이 원칙입니다.

실수 2: Determination에서 %control 직접 읽으려는 시도

Determination 핸들러의 importing 파라미터는 keys 형태이며, 일반적으로 %control에 직접 접근하지 않습니다. 대신 trigger 조건(determine on modify { field base_price; })을 BDEF에서 명시하여 "어떤 필드 변경 시 이 determination이 호출될지"를 선언적으로 정의합니다.

실수 3: Draft 시나리오 혼동

Draft가 활성화된 BO에서 사용자가 Draft를 편집하는 동안에는 활성(active) 인스턴스가 아닌 Draft 인스턴스에 대해 PATCH가 발생합니다. %control은 Draft 인스턴스 기준으로 채워지므로, "Draft에서 한 번이라도 변경된 필드"가 mk-on이 됩니다. Activate 시점의 save_modified에서는 Draft 전체가 활성으로 복사되므로 별도 %control 비교 대신 누적된 변경을 일괄 처리하는 패턴을 사용합니다.

실수 4: readonly 필드에서 mk-on 기대하기

BDEF에서 field ( readonly ) base_price;로 표시된 필드는 클라이언트가 값을 보내더라도 %control이 항상 mk-off로 들어옵니다. 또한 deep insert(헤더+아이템 동시 생성) 시에는 CREATE 핸들러가 호출되므로 %control 대신 모든 필드가 의미 있는 값으로 채워져 있다고 가정해야 합니다.

8. 실무에서 %control을 활용하는 고급 시나리오

실전에서 %control은 단순한 "변경 감지" 도구를 넘어 다음과 같은 고급 시나리오의 기반이 됩니다.

고급 패턴 1: OData Side Effect와의 연동

특정 필드가 변경됐을 때 UI에서 다른 영역을 재조회하도록 트리거하는 메타데이터를 BDEF/서비스 정의에 선언하고, 백엔드에서는 %control로 변경 여부를 재확인하여 비싼 재계산을 최소화합니다. base_price 변경 시 final_pricetax_amount를 Side Effect로 재조회하도록 선언하면, Fiori Elements UI는 자동으로 해당 필드를 갱신합니다.

고급 패턴 2: 필드 레벨 권한 분리

METHOD update_salesorder.
  LOOP AT entities ASSIGNING FIELD-SYMBOL(<e>).

    " 변경 시도된 필드와 사용자 권한 비교
    IF <e>-%control-discount_rate = if_abap_behv=>mk-on.
      TRY.
          AUTHORITY-CHECK OBJECT 'ZSO_DISC'
            ID 'ACTVT' FIELD '02'.
          IF sy-subrc <> 0.
            APPEND VALUE #(
              %tky = <e>-%tky
              %msg = new_message( id = 'ZSO' number = '001'
                                  severity = 'E' )
              %element-discount_rate = if_abap_behv=>mk-on
            ) TO reported-salesorder.
            INSERT <e>-%tky INTO TABLE failed-salesorder.
          ENDIF.
        CATCH cx_root.
      ENDTRY.
    ENDIF.

  ENDLOOP.
ENDMETHOD.

고급 패턴 3: 이벤트 페이로드 최소화

변경된 필드만 골라 이벤트 페이로드로 발행(RAISE ENTITY EVENT)하면 SAP Event Mesh나 외부 Kafka 토픽으로 흘러가는 메시지 크기가 줄어들고 수신자 측의 멱등성 처리가 단순해집니다. 전체 엔티티를 이벤트로 발행하는 대신 변경된 필드 목록과 새 값만 포함하는 delta 페이로드를 구성할 때 %control은 필수적인 판단 기준입니다.

다음으로 살펴볼 만한 인접 주제는 RAP의 Authorization Control(FOR INSTANCE AUTHORIZATION), Precheck, Side Effects in BDEF, 그리고 Event Binding입니다. 특히 strict(2) 모드의 BDEF에서는 %control과 관련된 컴파일 타임 체크가 더 엄격해지므로, 새 프로젝트에서는 가급적 strict(2)로 시작하는 것이 권장됩니다.

댓글 0

아직 댓글이 없습니다.