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 시나리오는 다음 상황에서 주로 사용됩니다.
- 레거시 BAPI/Function Module을 RAP으로 노출해야 할 때
- 외부 시스템 API를 RAP 엔티티로 감싸야 할 때
- 기존에 자체 트랜잭션 로직을 가진 비즈니스 객체를 마이그레이션할 때
핵심은 Entity Buffer와 SAVE 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 호출 시 finalize → check_save_allowed → save_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 호출 순서는 다음과 같습니다.
- finalize: 저장 직전 마지막 계산. Audit 필드, 자동 계산 필드 채우기
- check_save_allowed: 저장이 정말 가능한지 비즈니스 규칙 최종 검증
- adjust_numbers (선택): 임시 키를 최종 키로 치환
- save_modified: 실제 DB INSERT/UPDATE/DELETE 수행. 여기서 COMMIT WORK 호출 금지
- 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와 새로 생성된 OrderId를 mapped 테이블에 추가하지 않으면 호출자가 신규 키를 알 수 없어 후속 동작이 실패합니다.
실수 2: save_modified 안에서 COMMIT WORK 호출 — RAP Framework가 트랜잭션 경계를 관리하므로 명시적 COMMIT은 DB Inconsistency를 초래합니다. 반대로 ROLLBACK도 호출 금지.
실수 3: failed/reported 테이블 무시 — 유효성 검사 실패 시 failed와 reported에 결과를 채우지 않으면 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 unmanaged를 implementation managed로 변경하고, modify/read/save_modified 메서드를 단계적으로 제거하면서 RAP Framework에 위임합니다. 한 번에 모두 바꾸기보다는 엔티티 단위로 점진 마이그레이션이 안전합니다.
댓글 0
아직 댓글이 없습니다.