RAP

Unmanaged RAP에서 COMMIT WORK 쓰면 큰일 #shorts #SAP #RAP

▶ YouTube에서 보기

1. Unmanaged RAP가 등장한 배경

ABAP RESTful Application Programming Model(RAP)은 S/4HANA Cloud 및 ABAP Platform 2022 이상에서 비즈니스 객체를 선언적으로 정의하기 위한 프레임워크입니다. 일반적으로 신규 개발이라면 Managed 시나리오를 선택해 프레임워크가 영속화·잠금·번호 채번까지 자동으로 처리하도록 두는 것이 가장 빠른 길입니다. 그러나 현실에서는 이미 수십 년간 운영되어 온 레거시 트랜잭션(예: 자체 구축한 구매요청 처리 함수 모듈, BAPI_PO_CREATE1 같은 표준 BAPI, Z 테이블 기반 커스텀 클래스)이 그대로 살아 있는 경우가 훨씬 많습니다.

이런 자산을 RAP의 OData·Fiori 세계로 끌어올려야 할 때 등장하는 것이 Unmanaged Implementation Type입니다. Unmanaged는 "프레임워크가 CRUD 로직을 알아서 만들지 않는다"는 뜻이며, 개발자가 직접 ABAP 클래스에 SELECT·MODIFY·COMMIT WORK 호출까지 책임지고 작성해야 합니다. 이 글에서는 SalesOrder 시나리오를 기준으로 Unmanaged RAP에서 반드시 구현해야 하는 메서드 시퀀스, 엔티티 버퍼 처리, SAVE 단계의 순서를 단계별로 정리합니다.

  • 레거시 함수 모듈/BAPI를 OData 서비스로 노출시켜야 하는 경우
  • 표준 SAP가 제공하지 않는 트랜잭션을 RAP로 감싸야 하는 경우
  • 비즈니스 잠금·번호 채번 로직을 외부 시스템과 협업해야 하는 경우
  • 대용량 처리에서 프레임워크 기본 영속화 로직을 우회해야 하는 경우

2. 필요한 사전 이해와 환경

이 예제는 ABAP Cloud 또는 ABAP Platform 2022 이상(on-premise 2022/2023, S/4HANA Cloud Private/Public Edition)에서 ADT(ABAP Development Tools for Eclipse 3.32 이상)를 통해 작성한다고 가정합니다. CDS View Entity, Behavior Definition(BDEF), Behavior Implementation Class, Service Definition·Binding이 모두 같은 패키지에 존재해야 하며, 트랜잭션 SEGW가 아닌 ADT의 RAP 마법사를 통해 객체를 생성해야 합니다.

독자는 CDS View Entity 문법, ABAP OO(인터페이스 구현·LOCAL TYPES), Database Lock Object(SE11) 작성, COMMIT WORK 단위의 트랜잭션 개념을 알고 있어야 본문 코드를 무리 없이 따라갈 수 있습니다.

3. Managed와 Unmanaged의 결정적 차이

RAP의 implementation type은 크게 세 가지(Managed / Unmanaged / Managed with additional save)로 나뉩니다. 비유하자면 Managed는 풀서비스 호텔입니다. 예약·청소·체크아웃까지 호텔이 다 해줍니다. 반면 Unmanaged는 에어비앤비 자체 운영에 가깝습니다. 손님 응대(READ), 입실 기록(MODIFY/CREATE), 정산(SAVE)을 호스트가 직접 합니다. 그래서 더 유연하지만 빠뜨릴 수 있는 책임도 많습니다.

구체적으로 Unmanaged에서는 다음 모든 것을 직접 구현합니다.

  • READ: CDS에서 SELECT 한 후 result 테이블로 반환
  • CREATE / UPDATE / DELETE: in-memory 엔티티 버퍼에 적재 후 실제 DB I/O
  • LOCK: ENQUEUE 함수 모듈 직접 호출
  • NUMBERING (early/late): NUMBER RANGE 객체 또는 GUID 발급
  • SAVE 시퀀스: adjust_numbers → finalize → check_before_save → save → cleanup

그 대가로 얻는 자유는 레거시 BAPI 한 줄 호출만으로 트랜잭션을 끝낼 수 있다는 점입니다. 반대로 매번 직접 짜야 하므로 코드량이 늘고, 트랜잭션 버퍼 일관성(같은 키를 두 번 수정해도 동일한 결과여야 함)을 개발자가 보장해야 합니다.

4. Unmanaged 동작 원리: 한 번의 OData 요청이 흘러가는 길

Fiori Elements 화면에서 SalesOrder를 한 건 생성·수정·삭제하는 일괄 요청(OData $batch)이 들어오면 RAP 런타임은 다음 순서로 동작합니다. 이 순서를 머릿속에 두지 않으면 mapped/failed/reported 테이블 설계가 어그러집니다.

  1. OData → BO Runtime: 변경 요청을 entity-key 단위로 묶어 modify( ) 호출
  2. Implementation Class의 FOR MODIFY 메서드들이 호출됨 (create/update/delete 각각)
  3. 로컬 버퍼(예: mt_buffer)에 변경 내역 누적
  4. READ 요청이 들어오면 FOR READ가 호출되고, 버퍼와 DB를 합쳐서 반환
  5. 최종 저장 직전 RAP가 SAVE 단계로 진입: adjust_numbersfinalizecheck_before_savesavecleanup
  6. SAVE 단계에서 실제 INSERT/UPDATE/DELETE 또는 BAPI 호출, 그리고 COMMIT WORK

여기서 엔티티 버퍼(mapped/failed/reported)는 RAP 메서드 시그니처에 EXPORTING 파라미터로 등장하는 세 개의 테이블입니다. mapped는 신규 채번된 키를 클라이언트에 돌려줄 때, failed는 비즈니스 위반으로 처리 실패한 건을, reported는 사용자에게 메시지로 보여줄 정보를 담습니다.

5. 실전 1단계: BDEF에 unmanaged 선언과 클래스 골격

SalesOrder 헤더 한 개를 Unmanaged로 노출한다고 가정합니다. BDEF의 첫 줄에 implementation unmanaged를 명시합니다.

// ZBP_R_SalesOrderTP (Behavior Definition)
unmanaged implementation in class zbp_r_salesordertp unique;
strict ( 2 );

define behavior for ZR_SalesOrderTP alias SalesOrder
persistent table zord_header
lock master
authorization master ( instance )
etag master last_changed_at
{
  create;
  update;
  delete;

  field ( read only ) sales_order_id, created_at, created_by;
  field ( mandatory ) customer_id, requested_dlv_date;

  mapping for zord_header
  {
    sales_order_id      = sales_order_id;
    customer_id         = customer_id;
    requested_dlv_date  = requested_dlv_date;
    last_changed_at     = last_changed_at;
  }
}

그다음 Behavior Implementation 클래스에서는 LOCAL TYPES 블록에 핸들러 클래스를 정의합니다. Unmanaged의 핵심은 이 LOCAL CLASS 안의 메서드들입니다.

CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.

    TYPES: BEGIN OF ty_buffer_line,
             sales_order_id     TYPE zord_header-sales_order_id,
             customer_id        TYPE zord_header-customer_id,
             requested_dlv_date TYPE zord_header-requested_dlv_date,
             last_changed_at    TYPE zord_header-last_changed_at,
             change_mode        TYPE c LENGTH 1,   " C / U / D
             cid                TYPE abp_behv_cid,
           END OF ty_buffer_line.

    CLASS-DATA mt_buffer TYPE STANDARD TABLE OF ty_buffer_line WITH EMPTY KEY.

    METHODS create FOR MODIFY
      IMPORTING entities FOR CREATE SalesOrder.

    METHODS update FOR MODIFY
      IMPORTING entities FOR UPDATE SalesOrder.

    METHODS delete FOR MODIFY
      IMPORTING keys FOR DELETE SalesOrder.

    METHODS read FOR READ
      IMPORTING keys FOR READ SalesOrder
      RESULT result.

    METHODS lock FOR LOCK
      IMPORTING keys FOR LOCK SalesOrder.

ENDCLASS.

mt_buffer는 클래스 단위 정적 테이블이라는 점에 주의해야 합니다. RAP 런타임이 동일 LUW 안에서 같은 인스턴스를 유지하므로 정적이면 충분하지만, 멀티 BO를 한 클래스에 묶으면 충돌이 납니다.

6. 실전 2단계: FOR MODIFY / FOR READ 구현

FOR MODIFY는 들어온 entities 테이블을 버퍼에 적재하는 역할만 합니다. 실제 DB 쓰기는 SAVE 단계까지 미룹니다.

METHOD create.
  LOOP AT entities INTO DATA(ls_in).
    APPEND VALUE #(
      sales_order_id     = ''                          " late numbering
      customer_id        = ls_in-customer_id
      requested_dlv_date = ls_in-requested_dlv_date
      last_changed_at    = cl_abap_context_info=>get_system_date( )
      change_mode        = 'C'
      cid                = ls_in-%cid ) TO mt_buffer.
  ENDLOOP.
ENDMETHOD.

METHOD update.
  LOOP AT entities INTO DATA(ls_in).
    READ TABLE mt_buffer ASSIGNING FIELD-SYMBOL(<fs>)
      WITH KEY sales_order_id = ls_in-sales_order_id.
    IF sy-subrc <> 0.
      " 버퍼에 없으면 DB에서 끌어와서 적재
      SELECT SINGLE FROM zord_header
        FIELDS sales_order_id, customer_id, requested_dlv_date, last_changed_at
        WHERE sales_order_id = @ls_in-sales_order_id
        INTO @DATA(ls_db).
      APPEND VALUE #( BASE CORRESPONDING #( ls_db )
                      change_mode = 'U' ) TO mt_buffer ASSIGNING <fs>.
    ENDIF.
    IF ls_in-%control-customer_id = if_abap_behv=>mk-on.
      <fs>-customer_id = ls_in-customer_id.
    ENDIF.
    IF ls_in-%control-requested_dlv_date = if_abap_behv=>mk-on.
      <fs>-requested_dlv_date = ls_in-requested_dlv_date.
    ENDIF.
    <fs>-change_mode = COND #( WHEN <fs>-change_mode = 'C' THEN 'C' ELSE 'U' ).
  ENDLOOP.
ENDMETHOD.

%control 구조체 점검은 필드 단위 패치(OData PATCH)에서 누락 필드가 NULL로 덮어쓰는 사고를 막아 줍니다. 신규 개발자가 가장 자주 빠뜨리는 부분입니다.

READ 메서드는 DB와 버퍼를 합쳐 반환해야 합니다. 버퍼에 같은 키가 있으면 그쪽이 우선이며, change_mode가 'D'이면 결과에서 제외합니다.

METHOD read.
  SELECT FROM zord_header
    FIELDS sales_order_id, customer_id, requested_dlv_date, last_changed_at
    FOR ALL ENTRIES IN @keys
    WHERE sales_order_id = @keys-sales_order_id
    INTO TABLE @DATA(lt_db).

  LOOP AT keys INTO DATA(ls_key).
    READ TABLE mt_buffer INTO DATA(ls_buf)
      WITH KEY sales_order_id = ls_key-sales_order_id.
    IF sy-subrc = 0.
      IF ls_buf-change_mode = 'D'. CONTINUE. ENDIF.
      APPEND VALUE #( %tky               = ls_key-%tky
                      sales_order_id     = ls_buf-sales_order_id
                      customer_id        = ls_buf-customer_id
                      requested_dlv_date = ls_buf-requested_dlv_date ) TO result.
    ELSE.
      READ TABLE lt_db INTO DATA(ls_db)
        WITH KEY sales_order_id = ls_key-sales_order_id.
      IF sy-subrc = 0.
        APPEND VALUE #( %tky               = ls_key-%tky
                        sales_order_id     = ls_db-sales_order_id
                        customer_id        = ls_db-customer_id
                        requested_dlv_date = ls_db-requested_dlv_date ) TO result.
      ENDIF.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

7. 실전 3단계: LOCK, SAVE 시퀀스, 프로덕션 보강

SAVER 클래스는 별도의 LOCAL CLASS로 정의하며, BDEF에 with unique saver로 연결합니다. SAVE 흐름은 adjust_numbers(late numbering) → finalize(검증 직전 정리) → check_before_save(비즈니스 룰 최종 확인) → save(실제 INSERT/UPDATE/DELETE 또는 BAPI) → cleanup(버퍼 비우기) 순서입니다.

CLASS lsc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_saver.
  PROTECTED SECTION.
    METHODS adjust_numbers REDEFINITION.
    METHODS finalize       REDEFINITION.
    METHODS check_before_save REDEFINITION.
    METHODS save           REDEFINITION.
    METHODS cleanup        REDEFINITION.
ENDCLASS.

CLASS lsc_salesorder IMPLEMENTATION.

  METHOD adjust_numbers.
    " late numbering: 채번
    LOOP AT lhc_salesorder=>mt_buffer ASSIGNING FIELD-SYMBOL(<b>)
         WHERE change_mode = 'C'.
      CALL FUNCTION 'NUMBER_GET_NEXT'
        EXPORTING nr_range_nr = '01' object = 'ZORDHDR'
        IMPORTING number      = <b>-sales_order_id.
      APPEND VALUE #( %cid           = <b>-cid
                      %key-sales_order_id = <b>-sales_order_id )
             TO mapped-salesorder.
    ENDLOOP.
  ENDMETHOD.

  METHOD check_before_save.
    LOOP AT lhc_salesorder=>mt_buffer INTO DATA(ls_b)
         WHERE change_mode <> 'D'.
      IF ls_b-customer_id IS INITIAL.
        APPEND VALUE #( %key-sales_order_id = ls_b-sales_order_id )
               TO failed-salesorder.
        APPEND VALUE #( %key-sales_order_id = ls_b-sales_order_id
                        %msg = new_message_with_text(
                          severity = if_abap_behv_message=>severity-error
                          text = |Customer ID is mandatory| ) )
               TO reported-salesorder.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD save.
    DATA: lt_ins TYPE STANDARD TABLE OF zord_header,
          lt_upd TYPE STANDARD TABLE OF zord_header,
          lt_del TYPE STANDARD TABLE OF zord_header.
    LOOP AT lhc_salesorder=>mt_buffer INTO DATA(ls_b).
      CASE ls_b-change_mode.
        WHEN 'C'. APPEND CORRESPONDING #( ls_b ) TO lt_ins.
        WHEN 'U'. APPEND CORRESPONDING #( ls_b ) TO lt_upd.
        WHEN 'D'. APPEND CORRESPONDING #( ls_b ) TO lt_del.
      ENDCASE.
    ENDLOOP.
    INSERT zord_header FROM TABLE @lt_ins.
    UPDATE zord_header FROM TABLE @lt_upd.
    DELETE zord_header FROM TABLE @lt_del.
  ENDMETHOD.

  METHOD cleanup.
    CLEAR lhc_salesorder=>mt_buffer.
  ENDMETHOD.

ENDCLASS.

LOCK 메서드는 ENQUEUE 함수 모듈(SE11에서 lock object 생성 후 자동 생성된 ENQUEUE_EZORD_HEADER 형태)을 호출하고, 실패 시 failed 테이블에 담아야 합니다. 프로덕션 환경에서는 추가로 다음을 권장합니다.

  • 모든 DB 접근에 명시적 인덱스 힌트 또는 키 기반 조회만 사용
  • 버퍼 크기가 큰 경우 PACKAGE SIZE 기반 분할 처리
  • save 메서드 안에서 절대 COMMIT WORK 호출 금지 — 프레임워크가 책임집니다
  • ABAP Unit Test를 위해 CL_ABAP_BEHV_TEST_RUNNER로 EML 시나리오 자동화

8. 실수 사례, 트러블슈팅, 그리고 다음 행보

현장에서 반복적으로 만나는 문제는 다음과 같습니다.

  • Q1. mapped 테이블에 신규 키를 안 채워서 클라이언트가 "key not found" 오류를 봅니다. early numbering이든 late numbering이든 adjust_numbers 또는 create 메서드 내부에서 mapped-<entity>에 %cid와 새 키를 반드시 매핑해야 합니다. Fiori Elements는 이 매핑이 없으면 신규 행을 화면에 다시 못 그립니다.
  • Q2. save에서 COMMIT WORK를 직접 호출해 OData $batch가 깨집니다. Unmanaged라도 트랜잭션 경계는 프레임워크가 통제합니다. save에서는 DB I/O까지만 수행하고, COMMIT/ROLLBACK은 절대 코드에 쓰지 않아야 합니다.
  • Q3. 같은 키를 UPDATE 두 번 보냈더니 두 번째 변경이 사라집니다. update 메서드에서 버퍼를 키로 READ TABLE한 후 같은 라인을 갱신해야 합니다. 새 라인을 APPEND하면 마지막 라인이 우선되거나, save에서 두 번 UPDATE되어 오류가 납니다.

이외에 잠금 충돌이 나면 SM12에서 LUW별 ENQUEUE 객체를 확인하고, Behavior Pool 디버깅은 ADT의 RAP 디버거(브레이크포인트를 LOCAL CLASS 메서드에 직접) 또는 EML 콘솔에서 테스트하면 됩니다.

이 글에서 다룬 헤더 단건 Unmanaged를 익혔다면, 다음으로 확장할 만한 주제는 (1) 헤더-아이템 Composition Tree를 Unmanaged로 묶어 자식 엔티티의 변경을 부모 SAVE 한 번에 위임하기, (2) Managed with Additional Save로 일부만 직접 처리하는 하이브리드 전환, (3) Draft-Enabled BO에서의 Unmanaged 제약(현재 ABAP Platform에서는 Draft가 Managed에서만 지원되므로 우회 전략 설계)입니다.

참고할 만한 공식 문서와 커뮤니티 자료

댓글 0

아직 댓글이 없습니다.