RAP

$tmp vs DB — RAP 버퍼가 진짜 저장되는 순간 #shorts #SAP #RAP

▶ YouTube에서 보기

$tmp 버퍼, RAP 트랜잭션의 숨은 주인공

RAP(ABAP RESTful Application Programming Model)으로 Managed 시나리오를 개발하다 보면, 분명 CREATE를 실행했는데 DB 테이블을 SELECT하면 데이터가 없는 상황을 만나게 됩니다. 원인은 단 하나, 트랜잭셔널 버퍼(Transactional Buffer)입니다. 커뮤니티에서 흔히 "$tmp 버퍼"라고 부르는 이 영역은, 트랜잭션이 커밋되기 전까지 모든 변경 사항이 임시로 머무는 메모리 공간입니다. 참고로 BDEF 파생 타입에는 Late Numbering 시 임시 키를 담는 %tmp 컴포넌트가 실제로 존재하며, 이 글에서는 프레임워크가 관리하는 버퍼 전체와 %tmp 임시 키를 함께 다룹니다. RAP의 LUW는 Interaction Phase(변경 요청 수집)와 Save Sequence(영속화)로 나뉘는데, Interaction Phase 동안의 모든 상태는 DB가 아니라 버퍼에만 존재합니다. 이 원리를 이해하지 못하면 결정(Determination), 검증(Validation), EML 소비 코드 곳곳에서 "데이터가 안 보이는" 버그를 양산하게 됩니다. 이 글은 SAP BTP ABAP Environment 및 S/4HANA 2022 이상(ABAP 7.56+) 기준으로 작성했습니다.

Managed 시나리오에서의 버퍼 생애주기 — CREATE/MODIFY/DELETE 분기

Managed 시나리오의 핵심은 개발자가 버퍼를 직접 구현하지 않는다는 점입니다. 프레임워크가 CDS 뷰와 영속 테이블 매핑 정보를 바탕으로 버퍼를 자동 생성·관리합니다. 생애주기는 오퍼레이션별로 이렇게 분기됩니다.

  • CREATE — 새 인스턴스가 버퍼에 "신규" 플래그와 함께 적재됩니다. Internal Early Numbering이면 이 시점에 최종 키가, Late Numbering이면 임시 키(%tmp)가 부여되고 최종 키는 Save Sequence의 adjust_numbers에서 확정됩니다.
  • UPDATE — 프레임워크가 영속 테이블에서 원본을 읽어 버퍼에 올린 뒤, %control에 표시된 필드만 덮어씁니다. 이후 같은 트랜잭션의 READ는 이 버퍼 이미지를 반환합니다.
  • DELETE — 행이 즉시 지워지는 것이 아니라 버퍼에 "삭제 예정" 마킹만 됩니다. DB의 실제 DELETE는 Save Sequence까지 유예됩니다.

버퍼는 COMMIT ENTITIES 성공 또는 ROLLBACK ENTITIES, 그리고 Save Sequence 마지막 단계인 cleanup에서 소멸합니다. 즉 버퍼의 수명은 정확히 하나의 RAP LUW와 일치하며, 세션을 넘어 유지되는 것은 Draft 테이블(영속화된 버퍼 변형)뿐입니다.

버퍼와 영속 테이블이 만나는 순간 — 동기화 시점

버퍼와 DB의 동기화는 단 한 곳, Save Sequence에서만 일어납니다. 순서를 정확히 알아두면 디버깅이 쉬워집니다.

  1. COMMIT ENTITIES 실행 → Interaction Phase 종료
  2. finalize — 마지막 결정 로직 수행(버퍼 대상)
  3. check_before_save — 저장 전 검증. 실패 시 전체 롤백
  4. adjust_numbers — Late Numbering의 임시 키(%tmp)를 최종 키로 치환
  5. save — 이 시점에서야 프레임워크가 버퍼 내용을 INSERT/UPDATE/DELETE 문으로 변환해 DB에 반영
  6. DB COMMIT → cleanup으로 버퍼 해제

중요한 함의가 두 가지 있습니다. 첫째, Interaction Phase 중의 SELECT는 항상 "과거의 DB"를 봅니다. 버퍼의 최신 상태를 읽으려면 반드시 READ ENTITIES(EML)를 써야 합니다. 둘째, save 단계 이전에는 DB 락이 아닌 RAP의 글로벌 락(Lock Master 기반)이 동시성을 제어합니다. 일반적으로 이 구조 덕분에 짧은 DB 락 유지 시간과 긴 비즈니스 트랜잭션이 양립할 수 있습니다.

실전 예제: PurchaseOrder로 $tmp 동작 추적하기

구매오더 엔티티 ZI_PurchaseOrder(Managed, Late Numbering 가정)를 EML로 소비하며 버퍼 동작을 직접 확인해 보겠습니다.

CLASS zcl_po_buffer_demo IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
    " 1) Interaction Phase: 버퍼에만 CREATE
    MODIFY ENTITIES OF zi_purchaseorder
      ENTITY PurchaseOrder
        CREATE FIELDS ( Supplier Currency GrossAmount )
        WITH VALUE #( ( %cid        = 'PO_TMP_01'
                        Supplier    = '0001000123'
                        Currency    = 'KRW'
                        GrossAmount = '1500000.00' ) )
      MAPPED   DATA(ls_mapped)
      FAILED   DATA(ls_failed)
      REPORTED DATA(ls_reported).

    " 2) 같은 LUW 안에서 EML READ → 버퍼의 데이터가 보인다
    READ ENTITIES OF zi_purchaseorder
      ENTITY PurchaseOrder
        ALL FIELDS WITH CORRESPONDING #( ls_mapped-purchaseorder )
      RESULT DATA(lt_buffered).
    out->write( |버퍼 조회 건수: { lines( lt_buffered ) }| ). " 1

    " 3) 반면 직접 SELECT → 아직 DB에 없다
    SELECT COUNT(*) FROM zpo_tab INTO @DATA(lv_db_cnt).
    out->write( |DB 조회 건수: { lv_db_cnt }| ).             " 0

    " 4) COMMIT ENTITIES → Save Sequence에서 버퍼가 DB로 플러시
    COMMIT ENTITIES RESPONSE OF zi_purchaseorder
      MAPPED DATA(ls_late_mapped).
    " Late Numbering: ls_late_mapped에서 %tmp(임시 키) → 최종 키 매핑 확인
  ENDMETHOD.
ENDCLASS.

2번과 3번의 결과 차이가 이 글의 핵심입니다. 같은 데이터인데 EML은 1건, SQL은 0건을 반환합니다. EML READ는 버퍼와 DB를 병합해 "트랜잭션 관점의 현재 상태"를 돌려주지만, SQL은 커밋된 영속 상태만 보기 때문입니다. 또한 커밋 전 ls_mapped의 키는 임시 값이므로, 최종 문서번호는 COMMIT ENTITIES RESPONSE의 mapped(%tmp→최종 키 매핑)에서 읽어야 합니다.

버퍼 충돌과 ETag 검증 — 낙관적 잠금의 실체

버퍼는 세션별로 격리되어 있으므로, 두 사용자가 같은 구매오더를 수정하면 각자의 버퍼에 서로 다른 이미지가 생깁니다. 이 충돌을 잡아내는 장치가 ETag 기반 낙관적 잠금입니다.

managed implementation in class zbp_i_purchaseorder unique;
strict ( 2 );

define behavior for ZI_PurchaseOrder alias PurchaseOrder
persistent table zpo_tab
lock master
total etag LastChangedAt
etag master LocalLastChangedAt
{
  create;
  update;
  delete;
  field ( readonly, numbering : managed ) PoUuid;
}

동작 원리는 이렇습니다. 클라이언트가 데이터를 읽을 때 ETag 값(예: LocalLastChangedAt)을 함께 받고, UPDATE 요청 시 이 값을 되돌려 보냅니다. 프레임워크는 버퍼에 원본을 적재하는 시점에 DB의 현재 ETag와 요청의 ETag를 비교하고, 다르면 변경을 버퍼에 반영하지 않은 채 실패(HTTP 412 Precondition Failed에 해당)로 처리합니다. 즉 ETag 검증은 Save Sequence가 아니라 버퍼 적재 직전에 일어난다는 점이 중요합니다. total etag는 BO 전체 단위, etag master는 엔티티 단위 검증에 쓰이며, Managed 시나리오에서는 두 필드를 프레임워크가 자동 갱신하도록 managed 어노테이션과 함께 두는 것이 일반적으로 권장됩니다.

Managed Save vs Unmanaged Save — 버퍼가 비워지는 두 가지 방식

같은 Managed 시나리오라도 버퍼를 DB로 옮기는 마지막 단계는 두 방식으로 나뉩니다. 기본 Managed Save에서는 save 단계에서 프레임워크가 버퍼 델타를 분석해 SQL을 스스로 생성·실행합니다. 개발자가 개입할 지점은 with additional save(표준 저장 후 부가 로직) 정도입니다. 반면 BDEF에 with unmanaged save를 선언하면, 버퍼 관리·락·ETag·Draft는 프레임워크가 그대로 담당하되 최종 영속화만 개발자의 save_modified 메서드로 위임됩니다. 이때 메서드 파라미터로 전달되는 create/update/delete 테이블이 바로 "버퍼의 최종 스냅숏"입니다. 특히 update 테이블의 %control 구조에는 트랜잭션 동안 실제로 변경된 필드만 표시되므로, 이를 무시하고 전체 필드를 UPDATE하면 다른 세션의 변경을 덮어쓰는 사고가 납니다. 정리하면 — Managed Save는 "버퍼 해석까지 프레임워크", Unmanaged Save는 "버퍼 해석은 프레임워크, SQL은 개발자"이며, 어느 쪽이든 Interaction Phase의 버퍼 동작 자체는 완전히 동일합니다.

$tmp를 무시하면 만나는 버그 3종 세트

실수 1 — 결정/검증 로직에서 SQL로 자기 데이터 조회. 버퍼에만 있는 신규 인스턴스는 SELECT에 잡히지 않아 검증이 항상 통과(또는 항상 실패)합니다. 반드시 EML READ를 사용해야 합니다.

METHOD validate_amount.  " Validation on save
  " 잘못된 방식: DB에는 아직 없으므로 sy-subrc = 4
  " SELECT SINGLE gross_amount FROM zpo_tab WHERE po_uuid = ...

  " 올바른 방식: 버퍼+DB 병합 상태를 읽는다
  READ ENTITIES OF zi_purchaseorder IN LOCAL MODE
    ENTITY PurchaseOrder
      FIELDS ( GrossAmount Currency ) WITH CORRESPONDING #( keys )
    RESULT DATA(lt_po).
  LOOP AT lt_po INTO DATA(ls_po) WHERE GrossAmount <= 0.
    APPEND VALUE #( %tky = ls_po-%tky ) TO failed-purchaseorder.
    APPEND VALUE #( %tky = ls_po-%tky
                    %msg = new_message_with_text(
                             severity = if_abap_behv_message=>severity-error
                             text     = '금액은 0보다 커야 합니다' ) )
           TO reported-purchaseorder.
  ENDLOOP.
ENDMETHOD.

실수 2 — Interaction Phase에서 영속 테이블 직접 MODIFY. 버퍼를 우회해 MODIFY zpo_tab을 실행하면 버퍼와 DB가 어긋나고, Save Sequence에서 프레임워크가 낡은 버퍼 이미지로 덮어써 변경이 유실됩니다. Strict 모드(strict(2))에서는 금지된 문장으로 런타임 오류가 나기도 합니다. 실수 3 — Late Numbering인데 커밋 전 키를 화면/후속 로직에 사용. %tmp 임시 키는 커밋 후 무효가 되므로, 최종 키는 반드시 COMMIT ENTITIES RESPONSE의 mapped에서 변환해 써야 합니다. 마지막으로 COMMIT WORKCOMMIT ENTITIES를 혼용하지 않는 것도 기본 수칙입니다.

ABAP Debugger로 버퍼 속 들여다보기

버퍼는 프레임워크 내부 자료구조라 변수 하나로 열어볼 수는 없지만, ADT 디버거에서 다음 절차로 상태를 충분히 추적할 수 있습니다. (1) Behavior Pool의 결정/검증 메서드 또는 EML 호출부에 브레이크포인트를 겁니다. (2) 중단 시점에 keys, entities 파라미터의 %tky, %cid, %is_draft 값을 확인합니다 — %cid가 살아 있고 최종 키가 초기값이면 아직 버퍼 단계의 신규 인스턴스라는 뜻입니다. (3) 디버거에서 한 단계씩 진행하며 READ ENTITIES ... IN LOCAL MODE를 거친 RESULT 테이블을 열면 그 시점의 버퍼 병합 상태를 그대로 볼 수 있습니다. (4) Save Sequence 확인이 필요하면 save_modified(Unmanaged Save)나 adjust_numbers에 브레이크포인트를 걸어 %tmp→최종 키 매핑 테이블을 관찰합니다. Draft를 쓰는 BO라면 Draft 테이블을 SQL 콘솔로 조회하는 것이 "영속화된 버퍼"를 보는 가장 빠른 길입니다.

댓글 0

아직 댓글이 없습니다.