이 글에서 다루는 내용과 도달 지점
BOPF(Business Object Processing Framework)를 수년간 다뤄온 개발자가 RAP(ABAP RESTful Application Programming Model)로 넘어올 때 가장 먼저 부딪히는 벽은 라이프사이클의 재구성입니다. BOPF의 노드/액션/결정테이블에 익숙해진 손과 머리로 RAP의 Interaction Phase와 Save Phase를 보면, 이름이 비슷해 보여도 트랜잭션 경계, 버퍼 관리, 호출 순서가 미묘하게 다릅니다. 이 글은 그 차이를 깊이 있게 분석하고, SalesOrder 시나리오를 통해 양 프레임워크의 라이프사이클을 코드로 비교합니다.
- BOPF와 RAP의 라이프사이클 단계 매핑 이해
- Interaction Phase vs Save Phase 책임 분리 원칙
- Determination/Validation의 RAP 대응 개념 파악
- SalesOrder 시나리오로 Save Sequence 구현
- BOPF 마이그레이션 시 흔한 함정 식별
이 글을 따라가기 전 갖춰야 할 배경
BOPF 노드 구조, Determination/Validation/Action 개념, /BOBF/IF_TRA_TRANSACTION_MGR의 SAVE 흐름에 대한 실무 경험이 필요합니다. RAP 쪽으로는 Managed/Unmanaged BO의 차이, Behavior Definition(BDEF)과 Behavior Implementation(BIMP)의 기본 문법, CDS Entity 작성 경험이 있다면 본문 코드를 바로 이해할 수 있습니다. EML(Entity Manipulation Language)의 MODIFY, READ ENTITIES 구문도 미리 한 번 훑어두시길 권장합니다.
환경 구성과 버전 요건
이 글의 예제는 다음 환경을 기준으로 작성되었습니다. 라이프사이클 콜백 시그니처는 릴리스마다 미세하게 변경될 수 있으므로, 자사 환경의 버전을 반드시 확인하세요.
- ABAP Platform 2023 (on-premise) 또는 ABAP Cloud (BTP, ABAP Environment 2305 이상)
- ADT(ABAP Development Tools) 3.36 이상이 설치된 Eclipse
- RAP Managed 시나리오 기준 (Unmanaged는 별도 언급)
- BOPF 비교군은 S/4HANA 1909 기준 /BOBF/CL_TRA_TRANS_MGR_FACTORY API
- 예제 CDS 명명규칙은 ZI_/ZC_ 접두어 사용 (Interface/Consumption)
참고: ABAP Cloud(Steampunk)에서는 BOPF API가 더 이상 릴리스되지 않으며, RAP가 공식 후속으로 자리매김했습니다. 신규 개발은 RAP를 우선 고려하는 것이 일반적입니다.
두 프레임워크의 라이프사이클을 가르는 핵심 개념
BOPF의 트랜잭션은 크게 Modify(변경 누적) → Check Before Save → Save의 3단계로 흐릅니다. 변경은 즉시 내부 버퍼에 쌓이고, 각 단계에 등록된 Determination/Validation이 트리거 조건(REQUESTED, LOOP, FINALIZE 등)에 따라 호출됩니다. 이 모델의 장점은 세밀한 트리거 제어이지만, 단점은 결정테이블이 비대해질수록 호출 순서를 추적하기 어렵다는 점입니다.
반면 RAP는 라이프사이클을 단 두 개의 거대한 페이즈로 단순화합니다. Interaction Phase는 클라이언트(Fiori Elements, OData, EML 호출자)가 BO를 조작하는 모든 시간을 포괄합니다. CREATE, UPDATE, DELETE, Action 실행이 여기서 일어나며, 트랜잭션 버퍼는 BO 인스턴스 내부 또는 RAP Generic Buffer에 누적됩니다. 이 페이즈에서는 Determination on modify, Validation, Action이 호출됩니다.
핵심은 Save Phase가 시작되는 순간 BO는 "읽기 전용"이 된다는 점입니다. BOPF에서 SAVE 직전 Determination이 노드를 자유롭게 수정하던 습관으로 RAP에 접근하면, save_modified 같은 콜백에서 EML MODIFY가 통하지 않아 당황하게 됩니다. RAP의 Save Phase는 finalize → check_before_save → save → cleanup의 명확한 순서를 가지며, 이 중 finalize까지만 BO 수정이 허용되고 그 이후로는 DB 기록 외 어떤 BO 상태 변경도 불가능합니다.
비유하자면 BOPF는 "공장 안에서 끝까지 조립하는 라인"이고, RAP는 "조립과 출하 컨베이어가 물리적으로 분리된 라인"입니다. RAP의 분리는 트랜잭션 안정성과 동시성 제어를 강화하지만, BOPF에서 자유롭게 쓰던 "SAVE 직전 보정 로직"을 finalize 단계로 명확히 이동시켜야 합니다.
SalesOrder 시나리오로 보는 실전 코드
판매 주문 BO를 가정합니다. 주문 헤더(SalesOrder)와 주문 항목(SalesOrderItem)을 가지며, 총액 계산, 신용 한도 검증, 채번 로직이 필요합니다.
1단계 — 기본 Behavior Definition 작성
managed implementation in class zbp_i_salesorder unique;
strict ( 2 );
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
lock master
authorization master ( instance )
etag master last_changed_at
{
create;
update;
delete;
field ( numbering : managed, readonly ) order_uuid;
field ( readonly ) order_id, total_amount, created_at, created_by;
determination calculateTotal on modify { field item_amount; create; }
validation checkCreditLimit on save { create; field customer_id; }
action submitOrder result [1] $self;
mapping for zsalesorder corresponding
{
OrderUUID = order_uuid;
OrderID = order_id;
CustomerID = customer_id;
TotalAmount = total_amount;
}
}BOPF 개발자가 가장 먼저 확인할 부분은 determination on modify와 on save의 구분입니다. BOPF의 DT_REQUEST_BEFORE_SAVE와 유사한 "저장 직전" 트리거는 RAP에서 determination on save로 선언합니다.
2단계 — Interaction Phase 구현과 에러 처리
총액 계산 Determination과 신용 한도 Validation을 구현합니다. BOPF의 /BOBF/IF_FRW_MESSAGE 대신 RAP는 reported 구조체로 메시지를 전달합니다.
METHOD calculateTotal.
READ ENTITIES OF ZI_SalesOrder IN LOCAL MODE
ENTITY SalesOrder BY \_Item
FIELDS ( item_amount )
WITH CORRESPONDING #( keys )
RESULT DATA(items)
FAILED DATA(read_failed).
LOOP AT items INTO DATA(item) GROUP BY item-order_uuid.
DATA(sum_amount) = REDUCE decfloat34( INIT s = 0
FOR <ln> IN GROUP item NEXT s = s + <ln>-item_amount ).
MODIFY ENTITIES OF ZI_SalesOrder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( total_amount )
WITH VALUE #( ( %tky = VALUE #( order_uuid = item-order_uuid )
total_amount = sum_amount ) )
REPORTED DATA(update_reported).
ENDLOOP.
ENDMETHOD.
METHOD checkCreditLimit.
READ ENTITIES OF ZI_SalesOrder IN LOCAL MODE
ENTITY SalesOrder
FIELDS ( customer_id total_amount )
WITH CORRESPONDING #( keys )
RESULT DATA(orders).
LOOP AT orders INTO DATA(order).
SELECT SINGLE credit_limit FROM zcustomer_credit
WHERE customer_id = @order-customer_id
INTO @DATA(limit).
IF order-total_amount > limit.
APPEND VALUE #( %tky = order-%tky ) TO failed-salesorder.
APPEND VALUE #( %tky = order-%tky
%msg = NEW zcm_sales(
severity = if_abap_behv_message=>severity-error
textid = zcm_sales=>credit_exceeded
amount = order-total_amount )
%element-total_amount = if_abap_behv=>mk-on )
TO reported-salesorder.
ENDIF.
ENDLOOP.
ENDMETHOD.BOPF에서 ET_MESSAGE에 INSERT하던 패턴이 RAP에서는 reported 테이블에 APPEND하는 형태로 바뀝니다. %element-필드명 = mk-on 표기는 Fiori UI에서 어느 필드를 빨갛게 강조할지 지정하는 RAP만의 장치입니다.
3단계 — Save Phase와 채번, 그리고 트랜잭션 무결성
채번처럼 "저장 직전 한 번만" 일어나야 하는 로직은 Save Phase의 adjust_numbers 또는 finalize 메서드에서 처리합니다. Managed 시나리오에서 채번은 다음과 같이 분리됩니다.
CLASS lhc_SaveHandler IMPLEMENTATION.
METHOD adjust_numbers.
LOOP AT mapped-salesorder ASSIGNING FIELD-SYMBOL(<mapped>).
TRY.
cl_numberrange_runtime=>number_get(
EXPORTING nr_range_nr = '01'
object = 'ZSO_RANGE'
IMPORTING number = DATA(new_number) ).
<mapped>-order_id = new_number.
CATCH cx_number_ranges INTO DATA(lx_nr).
APPEND VALUE #( %cid = <mapped>-%cid
%msg = NEW zcm_sales(
severity = if_abap_behv_message=>severity-error
textid = zcm_sales=>nr_failed ) )
TO reported-salesorder.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD finalize.
" 저장 직전 마지막 보정. BO 수정 가능한 마지막 지점.
" 예: created_at, created_by 타임스탬프 확정
ENDMETHOD.
METHOD check_before_save.
" 모든 인스턴스 통합 검증. 여기서 실패하면 전체 트랜잭션 롤백.
" BO 수정 불가. 오직 reported/failed만 채울 수 있음.
ENDMETHOD.
METHOD save.
" DB INSERT/UPDATE/DELETE 실행. BO 수정 절대 불가.
INSERT zsalesorder FROM TABLE @( CORRESPONDING #( create ) ).
ENDMETHOD.
ENDCLASS.BOPF에서 채번 Determination을 DT_REQUEST_BEFORE_SAVE로 걸어두던 패턴이, RAP에서는 별도 SaveHandler의 adjust_numbers로 분리된다는 점에 주목하세요. 이는 단순한 위치 이동이 아니라 "채번은 Interaction Phase의 일반 로직과 다르다"는 책임 분리의 결과입니다. 프로덕션에서는 추가로 cleanup과 cleanup_finalize에서 인스턴스 캐시를 비우고, 단위 테스트는 CL_CDS_TEST_ENVIRONMENT와 CL_RAP_TEST_DOUBLES를 활용해 DB 의존성을 격리합니다.
BOPF 출신이 RAP에서 자주 빠지는 함정과 해결
첫 번째는 save 메서드에서 EML MODIFY를 시도하는 실수입니다. RAP의 save는 DB 쓰기 전용이며, BO 상태를 바꾸려는 시도는 dump 또는 무시됩니다. 보정 로직은 finalize로 옮기세요.
두 번째는 Determination on modify가 너무 자주 호출되는 문제입니다. BOPF의 LOOP 트리거 감각으로 모든 필드에 걸어두면, 한 번의 UPDATE에 수십 번 호출될 수 있습니다. trigger field를 최소화하고, 가능하면 on save로 미루는 것이 권장됩니다.
세 번째는 Unmanaged와 Managed의 혼동입니다. 기존 BOPF BO를 감싸 RAP로 노출할 때는 Unmanaged 시나리오를 선택해야 하며, 이 경우 save 메서드에서 BOPF Transaction Manager의 SAVE를 직접 호출해야 합니다.
FAQ. Q1. BOPF의 Action Validation이 RAP에서 어디로 갔나요? A. Action을 호출하기 전 사전 조건을 검사하려면 Feature Control(instance feature)을 활용하거나, Action 구현부 첫머리에서 직접 검증 후 reported에 메시지를 추가하세요. Q2. Determination 호출 순서를 보장할 수 있나요? A. RAP는 BOPF만큼 세밀한 순서 제어를 제공하지 않습니다. 의존 관계가 있다면 하나의 Determination 안에서 순차 처리하는 패턴이 일반적입니다. Q3. Draft 시나리오에서 라이프사이클이 달라지나요? A. Draft가 활성화되면 Interaction Phase가 Draft 테이블에서 실행되고, Activate 시점에 Active 테이블로 옮겨지며 별도의 검증 사이클이 한 번 더 돕니다.
여기서 더 깊이 들어갈 주제들
이 글에서 다룬 Managed 시나리오를 익혔다면, 다음은 Unmanaged Save with Additional Save 패턴으로 레거시 BOPF/Function Module과의 공존을 학습할 차례입니다. 또한 Side Effects와 Determine Action을 결합한 반응형 UI 패턴, OData v4 Draft 라이프사이클, 그리고 ABAP Unit + CL_RAP_TEST_DOUBLES 기반 BDD 테스트는 프로덕션 RAP 개발자가 반드시 거쳐야 하는 다음 단계입니다.
더 읽어볼 자료
댓글 0
아직 댓글이 없습니다.