개요 및 이 글에서 다룰 내용
SAP RAP(RESTful ABAP Programming Model)에서 Behavior 핸들러는 각각 독립적인 메서드로 호출되며, 같은 트랜잭션 안에서도 핸들러 간 직접적인 변수 공유가 불가능합니다. 특히 Determination 단계에서 계산한 값이나 외부 API에서 가져온 데이터를 Validation 단계에서 다시 사용해야 할 때, 동일한 계산을 반복하면 성능 저하가 발생합니다. 이 글에서는 Global Temp Buffer 패턴을 활용해 핸들러 간 데이터를 안전하게 전달하는 방법을 다룹니다.
- RAP 핸들러 호출 라이프사이클과 데이터 격리 원리 이해
- Static Class 기반 Temp Buffer 설계 및 구현
- Determination 결과를 Validation에서 재사용하는 실전 패턴
- 버퍼 무효화(invalidation) 전략과 메모리 누수 방지
- 병렬 처리(Parallel Processing) 환경에서의 주의사항
사전에 알아두면 좋은 내용
이 글은 RAP의 Managed/Unmanaged 시나리오 차이, Behavior Definition(BDEF)에서 determination·validation 선언 문법, 그리고 ABAP Static Attribute의 라이프사이클(세션 단위)에 대한 기본 이해를 전제로 합니다. EML(Entity Manipulation Language)의 MODIFY/READ 구문에 익숙하고, draft-enabled 시나리오와 active 시나리오의 차이를 알고 있어야 합니다. 또한 OO ABAP의 CLASS-DATA 키워드와 가비지 컬렉션 동작에 대한 기본 지식도 권장됩니다.
환경 / 버전 / 준비물
본 예제는 다음 환경 기준으로 작성되었습니다.
- SAP BTP ABAP Environment 2024 릴리스 또는 S/4HANA Cloud Private Edition 2023 이상
- ABAP Development Tools (ADT) 3.40 이상이 설치된 Eclipse
- RAP BO 생성 권한(개발자 역할 BR_DEVELOPER 또는 동등 권한)
- 테스트용 CDS View Entity 및 Behavior Definition 작성 권한
- 샘플 시나리오: 판매 주문(SalesOrderHdr) 엔티티에 신용 한도 체크 로직 적용
Steampunk(BTP ABAP Environment)와 온프레미스 S/4HANA의 ABAP 컴파일러 동작이 일부 다를 수 있으므로, 실제 배포 전 양쪽 환경에서 검증하는 것을 권장합니다.
핵심 개념: 왜 Global Temp Buffer가 필요한가
RAP 런타임은 한 번의 SAVE 호출 안에서 여러 핸들러를 순차적으로 호출합니다. 일반적인 호출 순서는 다음과 같습니다.
MODIFY요청 수신 → 인스턴스 생성/변경Determination on modify실행 (필드 자동 계산)Validation on save실행 (저장 직전 검증)save_modified/finalize핸들러 호출- DB 커밋
문제는 각 핸들러가 별도의 메서드로, 매개변수로 전달되는 키 테이블만 알 수 있다는 점입니다. 예를 들어 Determination에서 외부 신용평가 API를 호출해 "신용 등급 B, 한도 5000만원" 정보를 가져왔다면, Validation에서 "이 주문 금액이 한도를 초과하는가"를 체크할 때 동일한 API를 다시 호출해야 합니다. 같은 트랜잭션 내 동일 키에 대한 중복 호출은 응답 시간을 두 배로 늘리고, 외부 시스템의 호출 한도(rate limit)도 빠르게 소진시킵니다.
Global Temp Buffer는 일종의 "트랜잭션 스코프 메모(memo)"입니다. 도서관 사서가 책을 찾을 때마다 서가로 가지 않고, 자주 묻는 책의 위치를 책상 메모지에 적어두는 것과 같습니다. ABAP에서는 CLASS-DATA(static attribute)를 가진 별도 클래스를 만들어 핸들러 간에 데이터를 공유합니다. 단, 이 메모지는 세션이 끝나면 비워져야 하며, 잘못 관리하면 다음 트랜잭션이 이전 트랜잭션의 값을 잘못 읽는 stale data 문제가 발생합니다.
실전 코드 1단계: 기본 Buffer 클래스 설계
판매 주문 신용 평가 정보를 캐싱할 가장 단순한 형태의 버퍼 클래스를 작성합니다.
CLASS zcl_so_credit_buffer DEFINITION
PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
TYPES: BEGIN OF ty_credit_info,
order_uuid TYPE sysuuid_x16,
customer_id TYPE c LENGTH 10,
credit_rating TYPE c LENGTH 1,
credit_limit TYPE p LENGTH 13 DECIMALS 2,
fetched_at TYPE timestampl,
END OF ty_credit_info,
tt_credit_info TYPE HASHED TABLE OF ty_credit_info
WITH UNIQUE KEY order_uuid.
CLASS-METHODS:
get_instance RETURNING VALUE(ro_inst) TYPE REF TO zcl_so_credit_buffer,
put IMPORTING is_info TYPE ty_credit_info,
read IMPORTING iv_uuid TYPE sysuuid_x16
RETURNING VALUE(rs_info) TYPE ty_credit_info,
reset.
PRIVATE SECTION.
CLASS-DATA: mo_singleton TYPE REF TO zcl_so_credit_buffer,
mt_buffer TYPE tt_credit_info.
ENDCLASS.
CLASS zcl_so_credit_buffer IMPLEMENTATION.
METHOD get_instance.
IF mo_singleton IS INITIAL.
mo_singleton = NEW #( ).
ENDIF.
ro_inst = mo_singleton.
ENDMETHOD.
METHOD put.
DELETE mt_buffer WHERE order_uuid = is_info-order_uuid.
INSERT is_info INTO TABLE mt_buffer.
ENDMETHOD.
METHOD read.
READ TABLE mt_buffer INTO rs_info
WITH KEY order_uuid = iv_uuid.
ENDMETHOD.
METHOD reset.
CLEAR mt_buffer.
ENDMETHOD.
ENDCLASS.
HASHED TABLE을 사용한 이유는 키 조회가 O(1)로 일정한 성능을 보장하기 때문입니다. SORTED TABLE은 O(log n)이지만 트랜잭션당 수백 건이 누적되는 시나리오에서는 차이가 누적됩니다.
실전 코드 2단계: Determination과 Validation 연동
이제 실제 Behavior Implementation에서 버퍼를 활용합니다. Determination이 외부 API를 호출해 버퍼에 적재하고, Validation이 이를 읽어 검증합니다.
CLASS lhc_salesorderhdr DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS:
fetch_credit_info FOR DETERMINE ON MODIFY
IMPORTING keys FOR SalesOrderHdr~fetchCreditInfo,
validate_credit_limit FOR VALIDATE ON SAVE
IMPORTING keys FOR SalesOrderHdr~validateCreditLimit.
ENDCLASS.
CLASS lhc_salesorderhdr IMPLEMENTATION.
METHOD fetch_credit_info.
DATA(lo_buffer) = zcl_so_credit_buffer=>get_instance( ).
READ ENTITIES OF zi_salesorderhdr IN LOCAL MODE
ENTITY SalesOrderHdr
FIELDS ( customer_id total_amount )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_ord>).
TRY.
DATA(ls_credit) = zcl_credit_api=>fetch( <ls_ord>-customer_id ).
GET TIME STAMP FIELD DATA(lv_ts).
lo_buffer->put( VALUE #(
order_uuid = <ls_ord>-SalesOrderUUID
customer_id = <ls_ord>-customer_id
credit_rating = ls_credit-rating
credit_limit = ls_credit-limit
fetched_at = lv_ts ) ).
CATCH zcx_credit_api_error INTO DATA(lx_err).
cl_application_logger=>warn(
iv_msg = |Credit API failed for { <ls_ord>-customer_id }: { lx_err->get_text( ) }| ).
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD validate_credit_limit.
DATA(lo_buffer) = zcl_so_credit_buffer=>get_instance( ).
READ ENTITIES OF zi_salesorderhdr IN LOCAL MODE
ENTITY SalesOrderHdr
FIELDS ( total_amount )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_ord>).
DATA(ls_cached) = lo_buffer->read( <ls_ord>-SalesOrderUUID ).
IF ls_cached IS INITIAL.
APPEND VALUE #( %tky = <ls_ord>-%tky ) TO failed-salesorderhdr.
APPEND VALUE #( %tky = <ls_ord>-%tky
%msg = NEW zcm_so( severity = if_abap_behv_message=>severity-error
textid = zcm_so=>credit_info_missing ) )
TO reported-salesorderhdr.
CONTINUE.
ENDIF.
IF <ls_ord>-total_amount > ls_cached-credit_limit.
APPEND VALUE #( %tky = <ls_ord>-%tky ) TO failed-salesorderhdr.
APPEND VALUE #( %tky = <ls_ord>-%tky
%msg = NEW zcm_so( severity = if_abap_behv_message=>severity-error
textid = zcm_so=>credit_limit_exceeded
v1 = |{ ls_cached-credit_limit }| ) )
TO reported-salesorderhdr.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
핵심 포인트는 두 가지입니다. 첫째, Determination에서 API 실패 시에도 트랜잭션을 중단하지 않고 로깅 후 진행합니다. 둘째, Validation에서 버퍼 미적재 케이스(ls_cached IS INITIAL)를 명시적으로 처리해 fail-safe하게 동작하도록 만듭니다.
실전 코드 3단계: TTL과 버퍼 무효화, 동시성 대응
프로덕션 환경에서는 단순 누적 버퍼가 메모리를 잠식하고, draft 시나리오에서는 같은 키가 여러 번 재계산되어야 할 수도 있습니다. TTL(Time To Live)과 명시적 무효화 메서드, 그리고 트랜잭션 종료 시 자동 정리 로직을 추가합니다.
CLASS zcl_so_credit_buffer DEFINITION
PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
INTERFACES if_abap_session_uuid.
CONSTANTS c_ttl_seconds TYPE i VALUE 30.
CLASS-METHODS:
get_instance RETURNING VALUE(ro_inst) TYPE REF TO zcl_so_credit_buffer,
put IMPORTING is_info TYPE ty_credit_info,
read IMPORTING iv_uuid TYPE sysuuid_x16
RETURNING VALUE(rs_info) TYPE ty_credit_info,
invalidate IMPORTING iv_uuid TYPE sysuuid_x16,
reset_all,
cleanup_expired.
PRIVATE SECTION.
CLASS-DATA: mo_singleton TYPE REF TO zcl_so_credit_buffer,
mt_buffer TYPE tt_credit_info,
mv_session TYPE sysuuid_x16.
ENDCLASS.
CLASS zcl_so_credit_buffer IMPLEMENTATION.
METHOD get_instance.
DATA(lv_current_session) = cl_abap_session_uuid=>get( ).
IF mv_session <> lv_current_session.
CLEAR mt_buffer.
mv_session = lv_current_session.
ENDIF.
IF mo_singleton IS INITIAL.
mo_singleton = NEW #( ).
ENDIF.
ro_inst = mo_singleton.
ENDMETHOD.
METHOD read.
READ TABLE mt_buffer INTO rs_info WITH KEY order_uuid = iv_uuid.
IF sy-subrc = 0.
GET TIME STAMP FIELD DATA(lv_now).
DATA(lv_diff) = cl_abap_tstmp=>subtract( tstmp1 = lv_now
tstmp2 = rs_info-fetched_at ).
IF lv_diff > c_ttl_seconds.
DELETE mt_buffer WHERE order_uuid = iv_uuid.
CLEAR rs_info.
ENDIF.
ENDIF.
ENDMETHOD.
METHOD invalidate.
DELETE mt_buffer WHERE order_uuid = iv_uuid.
ENDMETHOD.
METHOD cleanup_expired.
GET TIME STAMP FIELD DATA(lv_now).
LOOP AT mt_buffer ASSIGNING FIELD-SYMBOL(<ls_b>).
IF cl_abap_tstmp=>subtract( tstmp1 = lv_now
tstmp2 = <ls_b>-fetched_at ) > c_ttl_seconds.
DELETE mt_buffer USING KEY primary_key.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
세션 ID 비교 로직은 워크 프로세스가 재사용되며 다른 사용자의 트랜잭션을 받을 때 데이터 누수가 발생하는 것을 방지합니다. 또한 RAP의 cleanup 추가 메서드를 BDEF에 선언하면 트랜잭션 종료 시 자동으로 reset_all을 호출하도록 연결할 수 있습니다.
METHOD cleanup REDEFINITION.
zcl_so_credit_buffer=>reset_all( ).
ENDMETHOD.
METHOD cleanup_finalize REDEFINITION.
zcl_so_credit_buffer=>reset_all( ).
ENDMETHOD.
유닛 테스트에서는 ABAP Unit의 CLASS_SETUP / TEARDOWN에서 reset_all을 호출해 테스트 간 격리를 보장하고, cl_osql_test_environment와 함께 buffer mock을 주입하는 패턴을 권장합니다.
흔한 실수 / 트러블슈팅 FAQ
Q1. Validation에서 항상 버퍼가 비어있다고 나옵니다. 가장 흔한 원인은 Determination이 on save가 아닌 on modify로만 선언되어 있고, 해당 필드 변경이 트리거되지 않아 Determination이 호출되지 않는 경우입니다. BDEF에서 trigger 필드를 명시하거나, determination on save를 추가해 Validation 직전에 항상 실행되도록 보장하는 것이 안전합니다.
Q2. 한 사용자의 데이터가 다른 사용자에게 보입니다. Static attribute는 워크 프로세스 메모리에 살아있기 때문에, 세션 식별자 없이 사용하면 stale data가 발생합니다. 3단계 코드처럼 cl_abap_session_uuid로 세션 변경을 감지하거나, BTP ABAP Environment에서는 cl_abap_context_info=>get_user_technical_name( ) 비교를 추가해야 합니다.
Q3. Parallel Processing(aRFC, bgPF) 시나리오에서 데이터가 전달되지 않습니다. Static 변수는 워크 프로세스 로컬이므로 병렬 처리로 분기된 작업에는 공유되지 않습니다. 이 경우 Shared Memory Object(CREATE SHARED MEMORY), 또는 DB 기반 임시 테이블(예: tt_buffer 트랜잭션 테이블)을 활용하는 것을 권장합니다. RAP managed runtime은 일반적으로 단일 워크 프로세스에서 실행되지만, late numbering이나 background job 트리거 시 동작이 달라질 수 있습니다.
Q4. Draft 시나리오에서 Active 인스턴스의 버퍼가 잘못 사용됩니다. Draft UUID와 Active UUID는 다르므로 키로 사용 시 문제가 없지만, "같은 비즈니스 키지만 다른 인스턴스 ID"를 가진 케이스에서는 customer_id 같은 비즈니스 키를 보조 인덱스로 두는 것이 좋습니다.
심화 학습 방향
Global Temp Buffer 패턴을 익혔다면 다음 주제로 확장해보는 것을 권장합니다. 첫째, Shared Memory Objects(SHMO)를 활용한 인스턴스 간 캐싱 — 마스터 데이터처럼 수정 빈도가 낮은 데이터에 적합합니다. 둘째, RAP Saver Class의 cleanup 메서드를 통한 lifecycle 통합 관리. 셋째, ABAP Channels(AMC/APC)를 활용한 트랜잭션 간 이벤트 기반 데이터 전파. 넷째, OData $batch 요청에서 여러 엔티티가 같은 버퍼를 공유할 때의 트랜잭션 경계 설계도 흥미로운 주제입니다.
더 읽어볼 자료
댓글 0
아직 댓글이 없습니다.