RAP

RAP COMMIT 원리 — 개발자 90%가 모르는 내막 #shorts #SAP #RAP

개요와 이 글에서 얻어갈 것

ABAP RESTful Application Programming Model(RAP)을 처음 다루는 개발자가 가장 자주 묻는 질문 중 하나는 "내 코드 어디에도 COMMIT WORK가 없는데 왜 DB에 데이터가 저장되어 있는가?"이다. 전통적인 ABAP 개발에서는 UPDATE, INSERT, MODIFY 후 명시적으로 COMMIT WORK를 호출해야 LUW(Logical Unit of Work)가 종결되었다. 그러나 RAP에서는 이 호출이 보이지 않는 곳에서 프레임워크가 대신 처리한다. 이 글에서는 RAP의 Interaction Phase와 Save Sequence가 어떻게 분리되어 동작하는지, Managed BO와 Unmanaged BO에서 COMMIT이 어느 시점에 누구에 의해 트리거되는지 구조적으로 살펴본다.

  • RAP의 Interaction Phase와 Save Sequence 구분 이해
  • EML(Entity Manipulation Language) 호출 시 트랜잭션 흐름 파악
  • Managed vs Unmanaged 시나리오의 COMMIT 책임자 분리
  • finalize / check_before_save / save / cleanup 단계별 메서드 활용
  • OData Gateway와 RAP Runtime이 commit_work를 호출하는 지점 추적

이 글을 읽기 전 갖춰두면 좋은 배경

ABAP CDS View, Behavior Definition(BDEF) 문법, Behavior Implementation 클래스(lhc_*), EML MODIFY ENTITIES / COMMIT ENTITIES 문장에 대한 기본적인 사용 경험이 있어야 한다. 또한 SAP LUW와 DB LUW의 차이, SET UPDATE TASK LOCAL, CALL FUNCTION ... IN UPDATE TASK와 같은 비동기 업데이트 메커니즘을 한 번이라도 다뤄본 경험이 있다면 본 내용의 흐름이 훨씬 명확해진다.

실습 환경과 버전 가정

본 글의 코드와 설명은 다음 환경을 기준으로 한다.

  • ABAP Platform 2023 (on-premise) 또는 SAP BTP ABAP Environment 2402 이상
  • ADT(ABAP Development Tools) 3.40 이상이 설치된 Eclipse
  • RAP 시나리오 유형: Managed with internal numbering, Unmanaged(레거시 BAPI 래핑) 양쪽 비교
  • OData V4 서비스 바인딩 및 Fiori Elements Preview 사용

핵심 개념: 왜 COMMIT이 보이지 않는가

RAP를 이해하는 가장 좋은 비유는 "음식점 주방"이다. 손님(Consumer, 예: Fiori Elements)이 주문을 하면 홀 직원이 주문서를 모으고(Interaction Phase), 정해진 시점에 한꺼번에 주방으로 넘긴다(Save Sequence). 손님이 직접 주방 가스불을 켜고 끄지 않는다. RAP에서는 이 "주방 가스불"이 바로 COMMIT WORK이며, 프레임워크가 적절한 타이밍에 켜고 꺼준다.

RAP Runtime은 트랜잭션을 명확히 두 단계로 나눈다.

  1. Interaction Phase: 클라이언트가 CREATE, UPDATE, DELETE, ACTION을 호출하는 단계. 이 시점의 변경분은 모두 RAP의 메모리 영역인 Transactional Buffer에 누적된다. DB는 아직 건드리지 않는다.
  2. Save Sequence: 클라이언트가 COMMIT ENTITIES를 호출하거나 OData Gateway가 PATCH/POST 응답 직전에 묵시적으로 호출하는 단계. 이때 프레임워크가 정해진 순서로 finalize → check_before_save → adjust_numbers → save → cleanup_finalize → cleanup를 실행하고, 마지막에 COMMIT WORK를 발행한다.

즉, 개발자가 작성하는 save 메서드 안에서 INSERT dbtab FROM TABLE를 실행해도 그것은 아직 DB LUW에 포함될 뿐이지, 실제 DB 트랜잭션이 종결된 것은 아니다. 종결은 RAP Runtime이 외부에서 호출하는 COMMIT WORK 한 번으로 일어난다. Managed 시나리오에서는 save 메서드조차 프레임워크가 자동 생성한 표준 구현을 사용하므로, 개발자 입장에서는 EML 한 줄로 모든 것이 끝나는 것처럼 보인다.

정리하면, RAP에서 보이지 않는 COMMIT의 정체는 "OData Request Handler" 또는 "EML COMMIT ENTITIES 구현체"가 Save Sequence 종료 후 발행하는 단 한 번의 COMMIT WORK이다.

실전 예제 1단계 — Managed 시나리오에서 COMMIT 흐름 관찰

먼저 가장 단순한 Managed BO를 정의한다. 사내 비품 대여 신청을 관리하는 ZI_EquipmentRequest를 가정한다.

managed implementation in class zbp_i_equipmentrequest unique;
strict ( 2 );

define behavior for ZI_EquipmentRequest alias EquipReq
persistent table zaequip_req
lock master
authorization master ( instance )
etag master LastChangedAt
{
  create;
  update;
  delete;

  field ( numbering : managed, readonly ) RequestUUID;
  field ( readonly ) CreatedBy, CreatedAt, LastChangedAt;

  mapping for zaequip_req
    {
      RequestUUID    = request_uuid;
      RequesterId    = requester_id;
      EquipmentCode  = equipment_code;
      RentStartDate  = rent_start_date;
      Status         = status;
      CreatedBy      = created_by;
      CreatedAt      = created_at;
      LastChangedAt  = last_changed_at;
    }
}

위 BDEF는 단 한 줄의 ABAP 핸들러 코드 없이도 동작한다. Save Sequence가 어떻게 흐르는지 확인하려면 다음과 같이 콘솔 보고서를 작성해 본다.

REPORT z_rap_commit_trace.

CLASS lcl_runner DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS main.
ENDCLASS.

CLASS lcl_runner IMPLEMENTATION.
  METHOD main.
    DATA lt_create TYPE TABLE FOR CREATE zi_equipmentrequest.

    APPEND VALUE #(
      %cid          = 'CID_001'
      RequesterId   = 'U-1001'
      EquipmentCode = 'NB-MAC-16'
      RentStartDate = cl_abap_context_info=>get_system_date( )
      Status        = 'REQ'
    ) TO lt_create.

    MODIFY ENTITIES OF zi_equipmentrequest
      ENTITY EquipReq
      CREATE FROM lt_create
      MAPPED   DATA(ls_mapped)
      FAILED   DATA(ls_failed)
      REPORTED DATA(ls_reported).

    IF ls_failed-equipreq IS INITIAL.
      COMMIT ENTITIES RESPONSE OF zi_equipmentrequest
        FAILED   DATA(ls_cfailed)
        REPORTED DATA(ls_creported).
      WRITE: / 'Persisted. UUID=', ls_mapped-equipreq[ 1 ]-RequestUUID.
    ELSE.
      ROLLBACK ENTITIES.
      WRITE: / 'Aborted.'.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  lcl_runner=>main( ).

이 보고서에는 COMMIT WORK가 한 줄도 등장하지 않지만 실행 직후 zaequip_req 테이블에 레코드가 저장된다. 비밀은 COMMIT ENTITIES다. 이 문장은 내부적으로 Save Sequence를 실행하고, 마지막 단계에서 COMMIT WORK를 호출한다. ST05 SQL Trace를 켜놓고 보면 MODIFY ENTITIES 시점에는 DB I/O가 없고, COMMIT ENTITIES 시점에 INSERTCOMMIT이 한 번에 떨어지는 것을 확인할 수 있다.

실전 예제 2단계 — Save 메서드 로깅과 에러 처리

실무에서는 저장 시점에 감사 로그를 남기거나, 외부 시스템에 알림을 발행해야 하는 경우가 많다. Managed 시나리오라도 additional save를 BDEF에 선언하면 Behavior Pool에 후크를 걸 수 있다.

define behavior for ZI_EquipmentRequest alias EquipReq
persistent table zaequip_req
with additional save
lock master
{
  create;
  update;
  delete;
  ...
}
CLASS lhc_equipreq DEFINITION INHERITING FROM cl_abap_behavior_saver.
  PROTECTED SECTION.
    METHODS save_modified REDEFINITION.
    METHODS check_before_save REDEFINITION.
    METHODS cleanup_finalize REDEFINITION.
ENDCLASS.

CLASS lhc_equipreq IMPLEMENTATION.
  METHOD check_before_save.
    LOOP AT create-equipreq INTO DATA(ls_new).
      SELECT SINGLE @abap_true
        FROM zaequip_req
        WHERE requester_id   = @ls_new-RequesterId
          AND equipment_code = @ls_new-EquipmentCode
          AND status         = 'REQ'
        INTO @DATA(lv_exists).
      IF lv_exists = abap_true.
        APPEND VALUE #(
          %tky = ls_new-%tky
          %fail-cause = if_abap_behv=>cause-unspecific
        ) TO failed-equipreq.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD save_modified.
    DATA lt_log TYPE TABLE OF zaequip_log.
    LOOP AT create-equipreq INTO DATA(ls_c).
      APPEND VALUE #(
        log_uuid       = cl_system_uuid=>create_uuid_x16_static( )
        request_uuid   = ls_c-RequestUUID
        event_type     = 'CREATE'
        event_at       = cl_abap_context_info=>get_system_utc_timestamp( )
        event_by       = cl_abap_context_info=>get_user_technical_name( )
      ) TO lt_log.
    ENDLOOP.
    INSERT zaequip_log FROM TABLE @lt_log.
  ENDMETHOD.

  METHOD cleanup_finalize.
    CLEAR: mt_buffer.
  ENDMETHOD.
ENDCLASS.

여기서 핵심은 INSERT zaequip_logCOMMIT WORK가 없다는 점이다. RAP Runtime이 Save Sequence 종료 후 단 한 번 COMMIT WORK를 호출하므로, 메인 테이블의 INSERT와 로그 테이블의 INSERT가 동일 DB LUW에 묶여 원자성을 보장받는다.

실전 예제 3단계 — Unmanaged 시나리오와 외부 호출 격리

레거시 BAPI나 함수 모듈을 래핑하는 Unmanaged 시나리오에서는 개발자가 save 메서드 안에서 직접 DB I/O를 수행한다. 이때 BAPI가 내부적으로 COMMIT WORK를 호출한다면 RAP Runtime과 충돌이 발생한다.

CLASS lhc_purchaseorder IMPLEMENTATION.
  METHOD save.
    DATA lt_po_items TYPE TABLE OF bapi_po_item.
    LOOP AT mt_create_buffer INTO DATA(ls_po).
      APPEND VALUE #(
        po_item   = ls_po-Item
        material  = ls_po-MaterialCode
        quantity  = ls_po-Quantity
      ) TO lt_po_items.
    ENDLOOP.

    CALL FUNCTION 'BAPI_PO_CREATE1'
      EXPORTING
        poheader = VALUE #( doc_type = 'NB' )
      TABLES
        poitem   = lt_po_items
        return   = DATA(lt_return).

    LOOP AT lt_return INTO DATA(ls_ret) WHERE type CA 'EA'.
      RAISE SHORTDUMP TYPE cx_abap_behv
        EXPORTING textid = cx_abap_behv=>save_failed.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

표준 BAPI 중 일부는 호출자가 직접 COMMIT WORK를 발행해야 동작이 완료된다. RAP Unmanaged에서 이런 BAPI를 호출하면 안 된다. 그 대신 BAPI의 "update task" 호출을 RAP Save Sequence의 끝까지 미루어, RAP Runtime이 발행하는 단 한 번의 COMMIT WORK에 합류하도록 만들어야 한다.

프로덕션 적용 시 점검할 포인트는 다음과 같다.

  • 성능: save_modified에서는 행 단위 INSERT보다 내부 테이블 일괄 INSERT ... FROM TABLE을 사용한다.
  • 테스트: CL_CP_TEST_ENVIRONMENT 기반의 RAP 테스트 더블을 사용하면 실제 DB를 건드리지 않고 EML 흐름을 검증할 수 있다.
  • 보안: authorization master로 인스턴스 권한 체크를 BDEF에 위임하고, save 메서드에서는 권한 SY-필드 직접 참조를 피한다.

현장에서 자주 부딪히는 실수와 해결

Q1. save 메서드 안에서 COMMIT WORK를 호출하면 어떻게 되는가?
RAP Runtime은 Save Sequence 도중 DB LUW가 끊기는 것을 가정하지 않는다. 임의 COMMIT은 후속 cleanup 호출 시점의 트랜잭션 컨텍스트를 손상시키고, ROLLBACK 가능성을 제거해 부분 저장이 발생할 수 있다. 절대 호출하지 않는다.

Q2. EML로 변경했는데 DB에 반영되지 않는다.
대부분의 원인은 COMMIT ENTITIES 누락이다. MODIFY ENTITIES는 Transactional Buffer까지만 반영하므로, 보고서/단위 테스트에서 명시적으로 COMMIT ENTITIES를 호출해야 한다. OData에서는 Gateway가 자동으로 호출하므로 신경 쓸 필요가 없다.

Q3. BAPI_TRANSACTION_COMMIT을 RAP에서 호출해도 되는가?
권장하지 않는다. 해당 함수는 즉시 COMMIT WORK를 발행하므로 RAP의 Save Sequence와 충돌한다. 대신 BAPI를 update task 모드로 호출하거나, 동기 호출 시에는 commit 파라미터를 OFF로 두고 RAP에 위임한다.

Q4. ROLLBACK ENTITIESROLLBACK WORK의 차이는?
전자는 Transactional Buffer만 비운다. 후자는 DB LUW를 되돌린다. Interaction Phase 중에는 ROLLBACK ENTITIES만으로 충분하다.

Q5. Draft 활성 BO에서는 어떻게 다른가?
Draft는 별도의 Draft 테이블에 임시 저장된다. SAVE 액션이 호출될 때 활성 테이블로 이전되며, 이 시점 역시 RAP Runtime이 COMMIT WORK를 발행한다. 개발자가 신경 쓸 일은 없다.

심화 학습 방향

본 글에서 다룬 Save Sequence를 충분히 이해했다면, 다음 영역으로 확장 학습하는 것을 권장한다.

  • RAP Late Numbering 시나리오 — adjust_numbers 메서드 활용
  • Draft-enabled BO의 Save와 Discard 흐름
  • Side Effects 정의를 통한 OData PATCH 최적화
  • RAP Business Events와 Outbox 패턴 결합한 이벤트 발행
  • 대량 처리 시나리오를 위한 EML IN LOCAL MODEAUGMENTING 동작

댓글 0

아직 댓글이 없습니다.