RAP

Unmanaged RAP CRUD 90%가 빠지는 함정 #shorts #SAP #RAP

▶ YouTube에서 보기

1. Managed vs Unmanaged — 무엇이 다른가

SAP의 ABAP RESTful Application Programming Model(이하 RAP)은 비즈니스 객체의 동작을 구현하는 두 가지 주요 시나리오를 제공합니다. Managed 시나리오에서는 RAP Framework가 자동으로 트랜잭션 버퍼, Lock 관리, 영속성 처리를 담당하지만, Unmanaged 시나리오에서는 이 모든 것을 개발자가 직접 구현해야 합니다.

이 글에서는 SAP S/4HANA 2023 (ABAP Platform 2023) 환경을 기준으로, 레거시 데이터베이스 테이블이나 기존 BAPI/Function Module을 RAP으로 끌어올리는 실전 예제를 통해 Unmanaged 구현에서 반드시 챙겨야 하는 포인트들을 단계별로 살펴봅니다.

  • BDEF에 Unmanaged 구현 타입 선언 방법 이해
  • MODIFY/READ ENTITY 핸들러 메서드 직접 구현
  • Entity Buffer를 CLASS-DATA로 관리하는 패턴 습득
  • SAVE SEQUENCE(finalize → check_save_allowed → save_modified) 정확한 호출 순서 학습
  • 레거시 BAPI를 RAP으로 감싸는 실전 래핑 패턴 적용

2. 시작하기 전 알아둘 것

이 글은 RAP의 기본 구성 요소(CDS View Entity, Behavior Definition, Behavior Implementation, Service Definition, Service Binding)에 대한 기본 이해를 전제로 합니다. Managed 시나리오 BDEF를 한 번이라도 작성해 본 경험이 있고, ABAP OO에서 Interface와 Class-Data 사용에 익숙하면 좋습니다. 또한 EML(Entity Manipulation Language)의 MODIFY ENTITIES, READ ENTITIES 구문을 본 적이 있으면 흐름을 따라가기 수월합니다.

3. 환경 / 버전 / 준비물

다음 환경을 기준으로 코드를 작성했습니다.

  • ABAP Platform: SAP S/4HANA 2023 On-Premise 또는 ABAP Cloud (Steampunk)
  • 개발 도구: ADT(ABAP Development Tools) for Eclipse 2024-03 이상
  • RAP 버전: Unmanaged with Additional Save 또는 Pure Unmanaged 시나리오
  • DB 테이블: zso_order (커스텀 테이블, 키 필드 order_id)
  • CDS View Entity: ZSalesOrder (테이블 기반 단일 엔티티)
  • 권한: ADT 개발자 권한, 패키지 생성/수정 권한

실습 전 zso_order 테이블을 SE11 또는 ADT의 Database Table 객체로 생성하고, 필드는 order_id (CHAR 10), customer_id (CHAR 10), total_amount (CURR 15,2), status (CHAR 2), created_by, created_at, last_changed_by, last_changed_at, local_last_changed_at를 포함해야 합니다.

4. 핵심 개념 — Framework가 멈추고 개발자가 시작하는 지점

Managed RAP을 자동 변속 차량에 비유하면, Unmanaged RAP은 수동 변속 차량입니다. Managed에서는 RAP Framework가 다음을 자동으로 처리합니다.

  • Transactional Buffer(트랜잭션 버퍼) 관리
  • CREATE/UPDATE/DELETE 시 메모리 변경 추적
  • SAVE 단계에서 DB COMMIT까지 일괄 처리
  • Lock(잠금) 객체 자동 호출

반면 Unmanaged 시나리오에서는 개발자가 다음 책임을 모두 떠안습니다.

"버퍼는 어디에 저장할 것인가, 키는 어떻게 매핑할 것인가, DB 반영은 언제 할 것인가, 실패는 어떻게 보고할 것인가 — 모든 결정이 개발자 손에 있습니다."

RAP Framework는 OData/Fiori 요청을 받아 BDEF에 정의된 동작 메서드를 호출하지만, 그 메서드 안에서 무슨 일이 일어나는지는 알지 못합니다. 이 때문에 Unmanaged 시나리오는 다음 상황에서 주로 사용됩니다.

  1. 레거시 BAPI/Function Module을 RAP으로 노출해야 할 때
  2. 외부 시스템 API를 RAP 엔티티로 감싸야 할 때
  3. 기존에 자체 트랜잭션 로직을 가진 비즈니스 객체를 마이그레이션할 때

핵심은 Entity BufferSAVE SEQUENCE 두 개념입니다. Entity Buffer는 한 LUW(Logical Unit of Work) 내에서 변경된 인스턴스를 모아두는 임시 저장소이며, SAVE SEQUENCE는 RAP Framework가 COMMIT 직전에 호출하는 3단계 약속된 흐름(finalize, check_save_allowed, save_modified)입니다.

5. 실전 예제 3단계

5-1. 1단계: BDEF에 Unmanaged 선언과 기본 골격

먼저 ZSalesOrder의 Behavior Definition을 작성합니다. implementation unmanaged 키워드가 핵심입니다.

unmanaged implementation in class zbp_salesorder unique;
strict ( 2 );

define behavior for ZSalesOrder alias SalesOrder
implementation in class zbp_salesorder unique
lock master
authorization master ( instance )
etag master LocalLastChangedAt
{
  create;
  update;
  delete;

  field ( readonly ) OrderId, CreatedBy, CreatedAt,
                     LastChangedBy, LastChangedAt, LocalLastChangedAt;

  mapping for zso_order
  {
    OrderId             = order_id;
    CustomerId          = customer_id;
    TotalAmount         = total_amount;
    Status              = status;
    LocalLastChangedAt  = local_last_changed_at;
  }
}

이제 Behavior Implementation Class의 골격을 정의합니다.

CLASS zbp_salesorder DEFINITION
  PUBLIC ABSTRACT FINAL FOR BEHAVIOR OF ZSalesOrder.

  PRIVATE SECTION.
    TYPES: BEGIN OF ty_buffer_line,
             order_id    TYPE zso_order-order_id,
             data        TYPE zso_order,
             change_mode TYPE c LENGTH 1,  " C/U/D
           END OF ty_buffer_line,
           ty_buffer TYPE SORTED TABLE OF ty_buffer_line
                     WITH UNIQUE KEY order_id.

    CLASS-DATA gt_buffer TYPE ty_buffer.
ENDCLASS.

5-2. 2단계: MODIFY ENTITY 핸들러와 READ ENTITY 핸들러 구현

Unmanaged 시나리오에서는 FOR MODIFY, FOR READ 메서드를 직접 작성해야 합니다. 다음은 SalesOrder의 CREATE/UPDATE/DELETE를 분기 처리하는 실전 코드입니다.

CLASS zbp_salesorder IMPLEMENTATION.

  METHOD modify FOR MODIFY
    IMPORTING entities_create FOR CREATE SalesOrder
              entities_update FOR UPDATE SalesOrder
              entities_delete FOR DELETE SalesOrder.

    " --- CREATE 분기 ---
    LOOP AT entities_create ASSIGNING FIELD-SYMBOL(<create>).
      DATA(lv_new_id) = generate_order_id( ).

      INSERT VALUE #(
        order_id    = lv_new_id
        change_mode = 'C'
        data        = VALUE #(
          order_id    = lv_new_id
          customer_id = <create>-CustomerId
          total_amount = <create>-TotalAmount
          status      = 'NW'
        )
      ) INTO TABLE gt_buffer.

      APPEND VALUE #(
        %cid    = <create>-%cid
        OrderId = lv_new_id
      ) TO mapped-salesorder.
    ENDLOOP.

    " --- UPDATE 분기 ---
    LOOP AT entities_update ASSIGNING FIELD-SYMBOL(<update>).
      READ TABLE gt_buffer ASSIGNING FIELD-SYMBOL(<buf>)
        WITH KEY order_id = <update>-OrderId.

      IF sy-subrc <> 0.
        SELECT SINGLE * FROM zso_order
          WHERE order_id = @<update>-OrderId
          INTO @DATA(ls_db).

        IF sy-subrc <> 0.
          APPEND VALUE #(
            %cid = <update>-%cid_ref
            %key = VALUE #( OrderId = <update>-OrderId )
          ) TO failed-salesorder.
          CONTINUE.
        ENDIF.

        INSERT VALUE #(
          order_id    = ls_db-order_id
          change_mode = 'U'
          data        = ls_db
        ) INTO TABLE gt_buffer
        ASSIGNING <buf>.
      ELSE.
        <buf>-change_mode = 'U'.
      ENDIF.

      IF <update>-%control-CustomerId = if_abap_behv=>mk-on.
        <buf>-data-customer_id = <update>-CustomerId.
      ENDIF.
      IF <update>-%control-TotalAmount = if_abap_behv=>mk-on.
        <buf>-data-total_amount = <update>-TotalAmount.
      ENDIF.
    ENDLOOP.

    " --- DELETE 분기 ---
    LOOP AT entities_delete ASSIGNING FIELD-SYMBOL(<delete>).
      READ TABLE gt_buffer ASSIGNING FIELD-SYMBOL(<del_buf>)
        WITH KEY order_id = <delete>-OrderId.

      IF sy-subrc = 0.
        <del_buf>-change_mode = 'D'.
      ELSE.
        INSERT VALUE #(
          order_id    = <delete>-OrderId
          change_mode = 'D'
        ) INTO TABLE gt_buffer.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.

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

    SELECT FROM zso_order
      FIELDS order_id, customer_id, total_amount, status,
             local_last_changed_at
      FOR ALL ENTRIES IN @keys
      WHERE order_id = @keys-OrderId
      INTO TABLE @DATA(lt_db).

    LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
      READ TABLE lt_db ASSIGNING FIELD-SYMBOL(<db>)
        WITH KEY order_id = <key>-OrderId.

      IF sy-subrc = 0.
        APPEND VALUE #(
          %key       = <key>-%key
          OrderId    = <db>-order_id
          CustomerId = <db>-customer_id
          TotalAmount = <db>-total_amount
          Status     = <db>-status
        ) TO result.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

5-3. 3단계: SAVE SEQUENCE와 프로덕션 안정성 보강

가장 중요한 3단계 SAVE SEQUENCE를 구현합니다. RAP Framework는 COMMIT ENTITIES 호출 시 finalizecheck_save_allowedsave_modified 순서로 호출합니다.

CLASS zbp_salesorder_saver DEFINITION
  PUBLIC ABSTRACT FINAL FOR BEHAVIOR OF ZSalesOrder.
ENDCLASS.

CLASS zbp_salesorder_saver IMPLEMENTATION.

  METHOD finalize.
    " 최종 계산: Status 자동 설정, Audit 필드 채우기
    LOOP AT zbp_salesorder=>gt_buffer ASSIGNING FIELD-SYMBOL(<buf>)
                                       WHERE change_mode <> 'D'.
      <buf>-data-last_changed_by       = sy-uname.
      <buf>-data-last_changed_at       = cl_abap_tstmp=>utclong2tstmp(
                                            utc_long = utclong_current( ) ).
      <buf>-data-local_last_changed_at = utclong_current( ).

      IF <buf>-change_mode = 'C'.
        <buf>-data-created_by = sy-uname.
        <buf>-data-created_at = <buf>-data-last_changed_at.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD check_save_allowed.
    " 비즈니스 룰: TotalAmount 음수 체크
    LOOP AT zbp_salesorder=>gt_buffer ASSIGNING FIELD-SYMBOL(<buf>)
                                       WHERE change_mode <> 'D'.
      IF <buf>-data-total_amount < 0.
        APPEND VALUE #(
          %key = VALUE #( OrderId = <buf>-order_id )
          %msg = new_message_with_text(
                   severity = if_abap_behv_message=>severity-error
                   text     = |Total amount cannot be negative| )
        ) TO reported-salesorder.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD save_modified.
    DATA: lt_insert TYPE STANDARD TABLE OF zso_order,
          lt_update TYPE STANDARD TABLE OF zso_order,
          lt_delete TYPE STANDARD TABLE OF zso_order.

    LOOP AT zbp_salesorder=>gt_buffer ASSIGNING FIELD-SYMBOL(<buf>).
      CASE <buf>-change_mode.
        WHEN 'C'. APPEND <buf>-data TO lt_insert.
        WHEN 'U'. APPEND <buf>-data TO lt_update.
        WHEN 'D'. APPEND <buf>-data TO lt_delete.
      ENDCASE.
    ENDLOOP.

    IF lt_insert IS NOT INITIAL.
      INSERT zso_order FROM TABLE @lt_insert.
    ENDIF.
    IF lt_update IS NOT INITIAL.
      UPDATE zso_order FROM TABLE @lt_update.
    ENDIF.
    IF lt_delete IS NOT INITIAL.
      DELETE zso_order FROM TABLE @lt_delete.
    ENDIF.

    CLEAR zbp_salesorder=>gt_buffer.
  ENDMETHOD.

ENDCLASS.

6. Entity Buffer 직접 관리와 SAVE SEQUENCE 흐름

위 예제에서 CLASS-DATA gt_buffer가 곧 Entity Buffer입니다. Managed 시나리오에서는 RAP Framework가 내부적으로 동일한 역할을 하지만, Unmanaged에서는 직접 관리해야 합니다. 주의할 점은 여러 LUW를 거치는 동안 버퍼가 살아 있어야 한다는 것이며, save_modified 후에는 반드시 CLEAR해야 다음 요청에 영향을 주지 않습니다.

SAVE SEQUENCE 호출 순서는 다음과 같습니다.

  1. finalize: 저장 직전 마지막 계산. Audit 필드, 자동 계산 필드 채우기
  2. check_save_allowed: 저장이 정말 가능한지 비즈니스 규칙 최종 검증
  3. adjust_numbers (선택): 임시 키를 최종 키로 치환
  4. save_modified: 실제 DB INSERT/UPDATE/DELETE 수행. 여기서 COMMIT WORK 호출 금지
  5. cleanup: 버퍼 비우기

7. 레거시 BAPI/FM 래핑 패턴

Unmanaged RAP의 진짜 위력은 기존 Function Module을 RAP 엔티티로 끌어올릴 때 나타납니다. BAPI_SALESORDER_CHANGE 같은 표준 BAPI를 save_modified 안에서 호출하는 패턴은 다음과 같습니다.

METHOD save_modified.
  LOOP AT zbp_salesorder=>gt_buffer ASSIGNING FIELD-SYMBOL(<buf>)
                                     WHERE change_mode = 'U'.
    DATA(ls_header) = VALUE bapisdh1x(
                        updateflag = 'U' ).
    DATA(ls_header_inx) = VALUE bapisdh1(
                            purch_no_c = <buf>-data-customer_id ).

    CALL FUNCTION 'BAPI_SALESORDER_CHANGE'
      EXPORTING
        salesdocument    = CONV vbeln_va( <buf>-order_id )
        order_header_in  = ls_header_inx
        order_header_inx = ls_header
      TABLES
        return           = DATA(lt_return).

    LOOP AT lt_return INTO DATA(ls_ret) WHERE type CA 'EA'.
      APPEND VALUE #(
        %key = VALUE #( OrderId = <buf>-order_id )
        %msg = new_message(
                 id       = ls_ret-id
                 number   = ls_ret-number
                 severity = if_abap_behv_message=>severity-error
                 v1       = ls_ret-message_v1 )
      ) TO reported-salesorder.
    ENDLOOP.
  ENDLOOP.
ENDMETHOD.

BAPI는 자체 COMMIT을 내부 호출하지 않도록 BAPI_TRANSACTION_COMMIT 호출을 절대 하지 마세요. RAP Framework가 마지막 단계에서 COMMIT을 책임집니다.

8. 자주 하는 실수와 주의사항

실수 1: mapped-salesorder에 키 매핑 누락 — CREATE 시 %cid와 새로 생성된 OrderIdmapped 테이블에 추가하지 않으면 호출자가 신규 키를 알 수 없어 후속 동작이 실패합니다.

실수 2: save_modified 안에서 COMMIT WORK 호출 — RAP Framework가 트랜잭션 경계를 관리하므로 명시적 COMMIT은 DB Inconsistency를 초래합니다. 반대로 ROLLBACK도 호출 금지.

실수 3: failed/reported 테이블 무시 — 유효성 검사 실패 시 failedreported에 결과를 채우지 않으면 Fiori UI는 성공으로 인식해 사용자에게 잘못된 피드백을 줍니다.

FAQ Q1. 버퍼를 GLOBAL DATA(ABAP Memory)로 관리해도 되나요?
권장하지 않습니다. CLASS-DATA로 선언하면 세션 내 유일성이 보장되지만, ABAP Memory를 쓰면 다른 트랜잭션과 충돌할 수 있습니다.

FAQ Q2. Unmanaged에서 Lock은 어떻게 처리하나요?
일반적으로 BDEF의 lock master 선언만으로는 부족합니다. FOR LOCK 메서드를 별도로 구현하거나 ENQUEUE_* Function Module을 modify 진입 시점에서 명시적으로 호출하는 패턴이 권장됩니다.

FAQ Q3. Managed로 마이그레이션해야 한다면 어떻게 접근하나요?
BDEF의 implementation unmanagedimplementation managed로 변경하고, modify/read/save_modified 메서드를 단계적으로 제거하면서 RAP Framework에 위임합니다. 한 번에 모두 바꾸기보다는 엔티티 단위로 점진 마이그레이션이 안전합니다.

댓글 0

아직 댓글이 없습니다.