RAP

BOPF vs RAP 라이프사이클 — 개발자가 헷갈리는 이유 #shorts #SAP #RAP

1. BOPF 라이프사이클 구조 — 과거의 표준

S/4HANA 초기와 ECC 기반 Fiori 1.0 시대를 거쳐온 개발자라면 BOPF(Business Object Processing Framework)는 익숙한 이름일 것입니다. BOPF는 비즈니스 오브젝트의 트랜잭션 처리를 일관된 방식으로 다루기 위해 SAP가 제공했던 ABAP 기반 프레임워크로, Node·Association·Determination·Validation·Action의 5요소를 중심으로 설계되었습니다. 라이프사이클 측면에서 BOPF는 비교적 명시적인 단계 분리를 강조했습니다. 클라이언트가 `/BOBF/CL_TRA_TRANSACTION_MGR`의 `save` 메서드를 호출하면 내부적으로 Check Before Save → Adjust Numbers → Save → Finalize의 순서로 진행되며, 이 사이에 등록된 Validation·Determination이 자동으로 트리거됩니다.

이 구조에서 개발자는 비교적 자유롭게 인스턴스 데이터를 메모리에 적재하고, Modify·Retrieve·Action 호출을 임의 순서로 섞을 수 있었습니다. BOPF의 트랜잭션 매니저가 In-memory 버퍼링과 변경 감지를 일관되게 처리해 주었기 때문입니다.

2. RAP 라이프사이클 개요 — 무엇이 달라졌나

ABAP RESTful Application Programming Model(RAP)은 BOPF의 후계자라기보다는 OData v4·CDS·Cloud-ready ABAP을 전제로 재설계된 새로운 모델입니다. 라이프사이클은 크게 Interaction PhaseSave Phase로 양분되며, 두 페이즈는 명확히 분리되어 호출 순서·허용 연산·트랜잭션 버퍼 접근 권한이 달라집니다. RAP에서는 EML(Entity Manipulation Language) 구문을 통해 모든 변경이 트랜잭션 버퍼에 기록되며, `COMMIT ENTITIES`(혹은 OData $batch 종료 시 자동 호출되는 commit)가 Save Phase의 시작점이 됩니다.

가장 큰 변화는 "단계별로 무엇을 할 수 있는가"가 엄격하게 정의되었다는 점입니다. BOPF가 "느슨한 규칙 + 매니저의 조율"이었다면, RAP은 "엄격한 페이즈 + 런타임 강제"에 가깝습니다.

3. Interaction Phase 상세 비교 (BOPF vs RAP)

Interaction Phase는 OData 요청(Create·Update·Delete·Action·Function)이 들어오면서 시작되어 `Modify` 호출로 트랜잭션 버퍼가 갱신되고, Determinations On Modify와 Validations On Save가 예약되는 단계입니다. BOPF에서는 Determination이 "After Modify", "Before Save", "After Loading" 등 다양한 시점에 결합될 수 있었고, Modify·Validate·Determine 사이의 경계가 다소 모호했습니다.

RAP에서는 이 단계가 훨씬 깔끔하게 정리됩니다. Interaction Phase에서는 다음이 허용됩니다.

  • EML `MODIFY ENTITIES` / `READ ENTITIES`로 트랜잭션 버퍼 변경·조회
  • RAP Action·Function 호출
  • Determination On Modify 실행 (트리거 필드 변경 시 자동)
  • RAP BO 인스턴스에 대한 Authorization·Feature Control 체크

반대로 이 단계에서 DB COMMIT, 외부 시스템 호출, 비결정적 작업(`GET TIME`, `cl_abap_random` 등)을 직접 수행하는 것은 권장되지 않습니다. RAP은 OData $batch 안에서 여러 요청을 묶어 처리하기 때문에, 중간 단계의 부수효과는 후속 요청 실패 시 롤백이 어렵습니다.

4. Save Phase / Save Sequence 비교 — 가장 큰 혼동 포인트

BOPF 개발자가 RAP에서 가장 많이 헤매는 부분이 바로 Save Sequence입니다. BOPF에서는 `save` 메서드 한 번으로 거의 모든 작업이 끝났지만, RAP에서는 Save Phase가 다시 세부 단계로 나뉩니다. 일반적으로 다음과 같은 순서로 진행됩니다.

  1. Finalize: 저장 직전 마지막 보정 로직. 키 할당, 계산 필드 갱신 등.
  2. Check Before Save: 저장 가능 여부 점검. 여기서 거부하면 전체 트랜잭션이 롤백.
  3. Adjust Numbers (Late Numbering 시): 최종 키 번호 부여. 임시 키(`%cid`)를 실키로 매핑.
  4. Save: 실제 DB INSERT/UPDATE/DELETE 수행.
  5. Cleanup: 버퍼 초기화.

Managed BO에서는 이 단계들이 프레임워크에 의해 자동 처리되지만, Unmanaged·Managed with Additional Save·Managed with Unmanaged Save 구현 타입에 따라 개발자가 직접 Save 핸들러를 구현해야 할 수 있습니다. BOPF의 "save() 한 방"에 익숙한 개발자가 RAP에서 `SAVE`·`FINALIZE`·`ADJUST_NUMBERS`를 따로 구현하라는 안내를 보고 당황하는 이유가 여기 있습니다.

핵심 차이: BOPF는 트랜잭션 매니저가 단계 호출을 추상화했지만, RAP은 Behavior Definition에서 어떤 단계를 사용할지 명시적으로 선언하고 Behavior Pool에서 구현해야 합니다.

5. Determinations과 Validations — 구조 변화

BOPF의 Determination은 클래스 단위(`/BOBF/IF_FRW_DETERMINATION`)로 구현되고, Configuration에서 Trigger Node·Trigger Field·Trigger Time을 지정하는 방식이었습니다. RAP에서는 Behavior Definition(`.bdef`) DDL 안에 선언적으로 작성합니다.

managed implementation in class zbp_i_salesorder unique;
strict ( 2 );
with draft;

define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
draft table zsalesorder_d
lock master
total etag LastChangedAt
authorization master ( instance )
{
  field ( numbering : managed, readonly ) SalesOrderId;
  field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
  field ( mandatory ) CustomerId, OrderDate;

  create;
  update;
  delete;

  action ( features : instance ) confirmOrder result [1] $self;
  action recalculateTotal;

  determination calculateTotalAmount on modify { field GrossAmount, TaxAmount; }
  determination setDefaultCurrency       on modify { create; }

  validation checkCustomerExists on save { field CustomerId; create; }
  validation checkOrderDate      on save { field OrderDate; create; update; }

  side effects
  {
    field GrossAmount affects field NetAmount, field TaxAmount;
  }
}

BOPF의 BOPF Builder UI에 흩어져 있던 메타데이터가 RAP에서는 한 파일로 응집됩니다. 가독성·버전 관리·코드 리뷰 측면에서 큰 개선입니다.

6. Actions: BOPF Function Module vs RAP Action Method

BOPF의 Action은 `/BOBF/IF_FRW_ACTION` 인터페이스 구현 클래스 + 환경설정으로 정의되었고, `do_action`이라는 단일 진입점에서 분기 처리하는 패턴이 일반적이었습니다. RAP에서는 각 Action이 Behavior Pool 클래스의 개별 메서드로 매핑되며, 시그니처는 `IMPORTING keys FOR ACTION` 형식을 따릅니다.

CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS confirmOrder FOR MODIFY
      IMPORTING keys FOR ACTION SalesOrder~confirmOrder RESULT result.

    METHODS recalculateTotal FOR MODIFY
      IMPORTING keys FOR ACTION SalesOrder~recalculateTotal.

    METHODS calculateTotalAmount FOR DETERMINE ON MODIFY
      IMPORTING keys FOR SalesOrder.

    METHODS checkCustomerExists FOR VALIDATE ON SAVE
      IMPORTING keys FOR SalesOrder~checkCustomerExists.
ENDCLASS.

BOPF 시절 한 클래스에 수십 개 Action을 우겨넣던 패턴은 RAP에서는 자연스럽게 메서드 단위로 분리되며, 단위 테스트(`CL_CDS_TEST_ENVIRONMENT`)가 쉬워집니다.

7. 실전 예제 — SalesOrder 시나리오로 보는 RAP 라이프사이클 구현

고객이 SalesOrder를 생성하고 confirmOrder 액션을 호출하면 (1) 라인아이템 합계 재계산 → (2) 신용한도 검사 → (3) 상태 코드 변경 → (4) 저장의 흐름이 일어난다고 가정하겠습니다. Interaction Phase와 Save Phase 양쪽에 코드가 분포하는 모습을 보면 BOPF와의 차이가 분명해집니다.

METHOD confirmOrder.
  " === Interaction Phase: 액션 본체 ===
  READ ENTITIES OF ZI_SalesOrder IN LOCAL MODE
    ENTITY SalesOrder
      FIELDS ( SalesOrderId CustomerId GrossAmount Status )
      WITH CORRESPONDING #( keys )
    RESULT DATA(orders)
    FAILED failed
    REPORTED reported.

  LOOP AT orders ASSIGNING FIELD-SYMBOL(<order>).
    IF <order>-Status = 'C'.
      APPEND VALUE #( %tky = <order>-%tky
                      %msg = NEW zcm_sales(
                        textid   = zcm_sales=>already_confirmed
                        severity = if_abap_behv_message=>severity-error )
                    ) TO reported-salesorder.
      CONTINUE.
    ENDIF.

    MODIFY ENTITIES OF ZI_SalesOrder IN LOCAL MODE
      ENTITY SalesOrder
        UPDATE FIELDS ( Status ConfirmedAt )
        WITH VALUE #( ( %tky        = <order>-%tky
                        Status      = 'C'
                        ConfirmedAt = cl_abap_context_info=>get_system_date( ) ) )
      REPORTED DATA(update_reported).
  ENDLOOP.

  " 후속 READ로 결과 반환
  READ ENTITIES OF ZI_SalesOrder IN LOCAL MODE
    ENTITY SalesOrder
      ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(refreshed).

  result = VALUE #( FOR r IN refreshed
                    ( %tky = r-%tky %param = r ) ).
ENDMETHOD.

METHOD calculateTotalAmount.
  " === Interaction Phase: Determination ===
  READ ENTITIES OF ZI_SalesOrder IN LOCAL MODE
    ENTITY SalesOrder BY \_Item
      FIELDS ( Quantity UnitPrice TaxRate )
      WITH CORRESPONDING #( keys )
    RESULT DATA(items).

  DATA totals TYPE TABLE FOR UPDATE ZI_SalesOrder.

  LOOP AT items INTO DATA(item) GROUP BY item-SalesOrderId.
    DATA(gross) = REDUCE decfloat34( INIT s = CONV decfloat34( 0 )
                                     FOR g IN GROUP item
                                     NEXT s = s + g-Quantity * g-UnitPrice ).
    APPEND VALUE #( SalesOrderId = item-SalesOrderId
                    %control-GrossAmount = if_abap_behv=>mk-on
                    GrossAmount  = gross ) TO totals.
  ENDLOOP.

  MODIFY ENTITIES OF ZI_SalesOrder IN LOCAL MODE
    ENTITY SalesOrder UPDATE FIELDS ( GrossAmount ) WITH totals.
ENDMETHOD.

METHOD checkCustomerExists.
  " === Save Phase 직전: Validation ===
  READ ENTITIES OF ZI_SalesOrder IN LOCAL MODE
    ENTITY SalesOrder FIELDS ( CustomerId )
      WITH CORRESPONDING #( keys )
    RESULT DATA(orders).

  SELECT customer_id FROM zcustomer
    FOR ALL ENTRIES IN @orders
    WHERE customer_id = @orders-CustomerId
    INTO TABLE @DATA(existing).

  LOOP AT orders ASSIGNING FIELD-SYMBOL(<o>).
    READ TABLE existing WITH KEY customer_id = <o>-CustomerId TRANSPORTING NO FIELDS.
    IF sy-subrc <> 0.
      APPEND VALUE #( %tky = <o>-%tky ) TO failed-salesorder.
      APPEND VALUE #( %tky = <o>-%tky
                      %state_area = 'CHECK_CUSTOMER'
                      %msg = NEW zcm_sales(
                        textid   = zcm_sales=>customer_not_found
                        severity = if_abap_behv_message=>severity-error )
                    ) TO reported-salesorder.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

그리고 Saver 클래스(Save Phase)는 BOPF의 `Save Sequence`에 직접 대응됩니다.

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

BOPF 개발자라면 `check_before_save`는 BOPF의 동명 단계와 같고, `save`는 실제 DB 반영, `cleanup`은 인스턴스 버퍼 비움이라는 점을 그대로 사용할 수 있습니다. 단 Managed BO에서는 이 메서드를 보통 건드리지 않습니다.

8. BOPF 개발자가 RAP 전환 시 반드시 알아야 할 핵심 차이

FAQ Q1. Interaction Phase에서 `COMMIT WORK`를 호출해도 되나요?
권장되지 않습니다. RAP은 $batch 단위 트랜잭션 일관성을 가정하므로 명시적 commit은 후속 단계 롤백을 망가뜨립니다. BOPF에서 종종 사용하던 부분 commit 패턴은 제거해야 합니다.

FAQ Q2. Determination 안에서 외부 BO를 변경해도 되나요?
RAP에서는 같은 트랜잭션 버퍼의 다른 RAP BO를 EML로 변경하는 것은 가능하지만, "Late" Determination이 아닌 한 사이드이펙트가 의도와 어긋날 수 있습니다. Side Effects 선언이나 `additional save` 핸들러로 옮기는 편이 안전합니다.

FAQ Q3. BOPF의 Reuse Library처럼 공용 로직을 어디에 두나요?
Behavior Pool은 OO 클래스이므로 별도 ABAP 클래스로 분리해 호출하면 됩니다. Saver·Handler가 비대해지지 않도록 도메인 서비스 클래스로 추출하는 패턴이 일반적입니다.

FAQ Q4. Strict 모드는 왜 필요한가요?
`strict ( 2 )`는 RAP BO가 보다 엄격한 런타임 체크를 받도록 합니다. BOPF에서는 허용되던 모호한 호출 시퀀스가 활성화 시점에 에러로 잡혀, 운영 환경에서 발생할 결함을 빌드 단계로 앞당깁니다.

마지막으로 학습 흐름을 정리하면, BOPF 개발자는 우선 Managed BO + Draft 시나리오로 RAP의 페이즈 분리를 체득한 뒤, Unmanaged·Managed with Additional Save로 점진적으로 확장하는 경로가 효율적입니다. ABAP Cloud로의 전환에서도 RAP은 핵심 모델이므로, S/4HANA Cloud Public Edition 2402 이상 또는 BTP ABAP Environment에서 실습 가능한 샘플 패키지(`DEMO_FLIGHT_*`, `RAP_DEMO_*`)를 함께 살펴보면 좋습니다.

관련 자료 및 참고 링크

댓글 0

아직 댓글이 없습니다.