이 글에서 다루는 내용
RAP(ABAP RESTful Application Programming Model)으로 만든 서비스에 여러 건의 변경을 한 번에 보냈는데, 딱 한 건의 검증 오류 때문에 나머지 아홉 건까지 전부 사라지는 경험을 해본 적이 있을 것입니다. 이것은 버그가 아니라 OData changeset과 RAP LUW(Logical Unit of Work)가 의도적으로 보장하는 원자성입니다. 이 글은 그 구조를 런타임 내부 동작까지 파고들어 설명합니다.
- OData $batch 안의 changeset이 왜 하나의 LUW로 묶이는지 이해한다
- RAP save sequence(finalize → check_before_save → adjust_numbers → save → cleanup)의 각 단계 역할을 파악한다
- 부분 저장(partial save)이 구조적으로 불가능한 이유와 우회 시도가 실패하는 지점을 확인한다
- 원자성을 전제로 한 실전 설계 패턴(changeset 분리, 사전 검증 강화)을 코드로 익힌다
시작 전에 갖추면 좋은 배경
RAP behavior definition의 기본 문법(managed/unmanaged, validation, determination)과 EML(Entity Manipulation Language)의 MODIFY ENTITIES, COMMIT ENTITIES 구문을 다뤄본 경험이 있으면 좋습니다. 클래식 ABAP의 SAP LUW 개념(COMMIT WORK의 의미)과 OData $batch 요청 포맷을 알고 있다면 이 글의 난이도는 크게 낮아집니다. 난이도는 advanced 수준입니다.
환경과 버전
이 글의 코드는 다음 환경을 기준으로 작성했습니다.
- SAP BTP ABAP Environment(Steampunk) 최신 릴리스 또는 SAP S/4HANA 2022 이상(ABAP Platform 7.57+,
strict(2)모드) - 개발 도구: ABAP Development Tools(ADT) for Eclipse — RAP 개발은 SE80이 아닌 ADT에서만 가능합니다
- 서비스 노출: OData V4 (Service Binding 유형
OData V4 - UI) — changeset 동작은 OData V2 배치에서도 동일한 원칙이 적용되지만, V4 기준으로 설명합니다 - 프런트엔드 예시는 SAPUI5의
sap.ui.model.odata.v4.ODataModel기준입니다
온프레미스 구버전(7.5x 초기)에서는 일부 save sequence 관련 문법이 다를 수 있으므로, 사용 중인 릴리스의 ABAP 키워드 문서를 함께 확인하는 것을 권장합니다.
핵심 개념 — changeset 하나가 곧 하나의 LUW다
OData $batch 요청은 택배 상자에 비유할 수 있습니다. 상자(batch) 안에는 여러 개의 밀봉 봉투(changeset)가 들어가고, 봉투 하나에는 여러 장의 전표(개별 POST/PATCH/DELETE 요청)가 들어갑니다. RAP 런타임은 봉투 단위로만 도장을 찍습니다. 봉투 안 전표 중 한 장이라도 결재가 거절되면, 그 봉투 전체가 반려됩니다. 봉투 안의 나머지 전표만 골라서 처리해 주는 창구는 존재하지 않습니다.
기술적으로 풀면 이렇습니다. RAP 트랜잭션은 두 단계로 나뉩니다.
- Interaction phase(상호작용 단계) — changeset 안의 각 요청이 순서대로 BO의 트랜잭셔널 버퍼에 쌓입니다. 이 시점에는 DB에 아무것도 기록되지 않습니다. 인스턴스 권한 체크, feature control, on-modify determination이 여기서 실행됩니다.
- Save sequence(저장 시퀀스) — changeset의 끝에 도달하면 런타임이 저장 시퀀스를 시작합니다. 순서는 ①
finalize(on-save determination 실행, 파생 데이터 최종 계산) → ②check_before_save(on-save validation 전부 실행) → ③adjust_numbers(late numbering 사용 시 최종 키 채번) → ④save(버퍼 내용을 DB 테이블에 기록) → 암묵적COMMIT WORK→ ⑤cleanup(버퍼 해제)입니다.
부분 저장이 불가능한 이유는 이 구조에 있습니다. ②번 단계에서 validation이 단 하나라도 failed를 리턴하면 런타임은 ③ 이후로 진행하지 않고 전체를 거부(rejected)한 뒤 cleanup(with rollback)으로 직행합니다. 트랜잭셔널 버퍼는 changeset 전체가 공유하는 하나의 작업 공간이고, DB 커밋은 ABAP LUW 특성상 세션당 전부-아니면-전무(all-or-nothing)입니다. "성공한 항목의 버퍼만 커밋"하려면 버퍼를 쪼개서 두 번 커밋해야 하는데, RAP 런타임은 changeset 경계에서 정확히 한 번의 커밋 지점만 갖도록 설계되어 있습니다. 이것이 OData 스펙이 요구하는 changeset 원자성을 ABAP LUW로 구현한 방식입니다.
우회 시도가 실패하는 이유도 여기서 나옵니다. behavior 구현 코드 안에서 COMMIT WORK나 ROLLBACK WORK를 직접 호출하면 RAP 런타임이 이를 감지해 런타임 오류(short dump)로 세션을 중단시킵니다. 커밋 시점의 제어권은 온전히 런타임에 있습니다.
실전 코드 3단계
1단계 — 기본: 원자성이 눈에 보이는 BO 만들기
배송 주문(ShipmentOrder)과 배송 품목(ShipmentItem)으로 구성된 managed BO를 예로 듭니다. 품목 중량 검증이 하나라도 실패하면 헤더 포함 전체가 롤백되는 구조입니다.
managed implementation in class zbp_r_shipmentorder unique;
strict ( 2 );
define behavior for ZR_ShipmentOrder alias ShipmentOrder
persistent table zship_order
lock master
authorization master ( instance )
etag master LastChangedAt
{
create; update; delete;
association _Item { create; }
validation validateRoute on save { create; update; field RouteCode; }
}
define behavior for ZR_ShipmentItem alias ShipmentItem
persistent table zship_item
lock dependent by _Order
authorization dependent by _Order
etag dependent by _Order
{
update; delete;
association _Order;
validation validateWeight on save { create; update; field GrossWeight; }
}
validation은 save sequence의 check_before_save 시점에 실행됩니다. failed 테이블에 키를 추가하는 순간, 이 changeset의 운명은 전체 롤백으로 결정됩니다.
METHOD validateWeight.
READ ENTITIES OF zr_shipmentorder IN LOCAL MODE
ENTITY ShipmentItem
FIELDS ( GrossWeight ) WITH CORRESPONDING #( keys )
RESULT DATA(lt_items).
LOOP AT lt_items INTO DATA(ls_item) WHERE GrossWeight > 25000.
APPEND VALUE #( %tky = ls_item-%tky ) TO failed-shipmentitem.
APPEND VALUE #(
%tky = ls_item-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |중량 { ls_item-GrossWeight }kg: 허용 한도 초과| )
%element-grossweight = if_abap_behv=>mk-on
) TO reported-shipmentitem.
ENDLOOP.
ENDMETHOD.
2단계 — 실무: additional save로 로깅하되, LUW를 존중하기
저장 성공 시 변경 이력을 별도 테이블에 남기고 싶다면 with additional save를 선언하고 saver 클래스에서 save_modified를 재정의합니다. 핵심은 여기서 쓰는 INSERT도 같은 LUW에 속한다는 점입니다. 이후 다른 BO의 save가 실패하면 이 로그도 함께 롤백되므로, "저장 실패했는데 로그만 남는" 불일치가 원천적으로 없습니다.
CLASS lsc_zr_shipmentorder DEFINITION
INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS save_modified REDEFINITION.
ENDCLASS.
CLASS lsc_zr_shipmentorder IMPLEMENTATION.
METHOD save_modified.
DATA lt_log TYPE STANDARD TABLE OF zship_chg_log.
LOOP AT create-shipmentorder INTO DATA(ls_new).
APPEND VALUE #( order_uuid = ls_new-OrderUUID
action = 'CREATE'
changed_at = utclong_current( ) ) TO lt_log.
ENDLOOP.
IF lt_log IS NOT INITIAL.
INSERT zship_chg_log FROM TABLE @lt_log.
" 주의: 여기서 COMMIT WORK 호출 시 즉시 런타임 오류.
" 커밋은 changeset 종료 시점에 런타임이 단 한 번 수행한다.
ENDIF.
ENDMETHOD.
ENDCLASS.
흔한 실수는 이 시점에 aRFC(CALL FUNCTION ... STARTING NEW TASK)로 외부 시스템에 알림을 쏘는 것입니다. save가 호출됐어도 같은 LUW의 다른 BO save가 뒤이어 실패하면 전체가 롤백되는데, 이미 날아간 알림은 되돌릴 수 없습니다. 외부 전파는 커밋 이후에 동작하는 RAP Business Event나 IN BACKGROUND UNIT 계열 메커니즘으로 미루는 것이 일반적으로 안전합니다.
3단계 — 프로덕션: changeset 분리 설계와 원자성 검증 테스트
업무 요구가 "10건 중 성공한 것만이라도 저장"이라면 서버에서 원자성을 깨려 하지 말고, 클라이언트에서 changeset을 분리해야 합니다. $batch 하나에 changeset을 여러 개 담으면 각각이 독립 LUW가 됩니다.
// SAPUI5 OData V4 — 행(row)마다 독립 updateGroup = 독립 changeset = 독립 LUW
const oModel = this.getView().getModel();
async function saveRowsIndependently(aRows) {
const aResults = await Promise.allSettled(
aRows.map((oRow, i) => {
const sGroup = `shipRow_${i}`;
const oBinding = oModel.bindList("/ShipmentOrder", undefined,
undefined, undefined, { $$updateGroupId: sGroup });
oBinding.create(oRow.payload);
return oModel.submitBatch(sGroup); // 그룹별 커밋 → 부분 성공 허용
})
);
// 실패 행만 UI에 오류 표시, 성공 행은 확정 처리
return aResults;
}
반대로 "헤더+품목은 반드시 함께"라면 하나의 그룹으로 묶어 원자성을 그대로 활용합니다. 원자성 자체가 회귀 테스트 대상이라는 점도 중요합니다. EML 기반 단위 테스트로 "하나 실패 시 전부 롤백"을 검증해 두면, 추후 validation 로직 수정이 원자성 계약을 깨는지 즉시 감지할 수 있습니다.
METHOD one_failure_rolls_back_all.
" 정상 1건 + 검증 위반 1건을 같은 LUW로 커밋 시도
MODIFY ENTITIES OF zr_shipmentorder
ENTITY ShipmentOrder
CREATE FIELDS ( RouteCode PlannedDate )
WITH VALUE #(
( %cid = 'GOOD' RouteCode = 'KR-01' PlannedDate = '20260915' )
( %cid = 'BAD' RouteCode = '' PlannedDate = '20260915' ) )
MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES RESPONSE OF zr_shipmentorder
FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
cl_abap_unit_assert=>assert_not_initial( commit_failed ).
SELECT COUNT(*) FROM zship_order INTO @DATA(lv_cnt).
cl_abap_unit_assert=>assert_equals( act = lv_cnt exp = 0 ).
" GOOD 건까지 0건 → 전체 롤백이 계약대로 동작함을 검증
ENDMETHOD.
보안·성능 관점에서는 changeset을 잘게 쪼갤수록 커밋 횟수와 네트워크 왕복이 늘어나므로, 대량 처리라면 사전 검증(precheck, draft 단계 validation)으로 실패율 자체를 낮춘 뒤 큰 changeset을 유지하는 편이 유리한 경우가 많습니다.
흔한 실수와 트러블슈팅 FAQ
Q1. save_modified 안에서 COMMIT WORK를 호출했더니 덤프가 납니다.
정상 동작입니다. RAP 런타임은 behavior 구현 내부의 명시적 커밋/롤백을 금지하며, 위반 시 BEHAVIOR_ILLEGAL_STATEMENT 계열 런타임 오류로 중단시킵니다. 커밋 시점을 코드로 앞당겨 부분 저장을 흉내 내려는 시도는 이 지점에서 전부 차단됩니다. 부분 저장이 필요하면 changeset을 분리하십시오.
Q2. validation은 통과했는데 저장이 안 되고 changeset 전체가 반려됩니다.
같은 changeset에 묶인 다른 엔터티의 validation 실패나, finalize 단계 determination에서 발생한 오류를 의심하십시오. save sequence는 changeset에 관여한 모든 BO에 대해 단계별로 함께 진행되므로, 내 엔터티가 깨끗해도 이웃이 실패하면 같이 롤백됩니다. reported 구조의 메시지를 전부 확인하는 것이 진단의 출발점입니다.
Q3. changeset 실패 응답에서 어떤 항목이 원인인지 어떻게 찾나요?
OData V4는 실패한 changeset에 대해 오류 응답을 반환하며, RAP은 reported에 담긴 %tky·%element 정보를 target 경로로 매핑해 줍니다. 프런트엔드에서는 메시지의 target을 파싱해 해당 행/필드에 하이라이트를 걸 수 있습니다. 서버 측에서는 ADT의 EML 콘솔이나 위 3단계의 단위 테스트로 재현하는 것이 빠릅니다.
Q4. 대량 업로드에서 한 건 때문에 9,999건이 날아가는 걸 막으려면?
세 가지 축으로 접근합니다. ① 클라이언트 changeset 분리(청크 단위 LUW), ② precheck와 draft 단계 검증으로 실패를 저장 전에 소진, ③ 실패 청크만 재시도하는 재처리 큐 설계. 서버에서 원자성을 끄는 옵션은 없다고 보는 것이 맞습니다.
더 깊이 파볼 주제
이 글의 원자성 개념을 잡았다면 자연스럽게 이어지는 주제는 다음과 같습니다. late numbering(adjust_numbers 단계에서의 최종 키 채번과 %pid 매핑), unmanaged save에서 기존 BAPI/함수 모듈을 save 단계에 통합할 때의 LUW 제약, 커밋 이후 비동기 전파를 담당하는 RAP Business Events, 그리고 draft-enabled BO에서 draft 저장과 active 저장의 LUW가 어떻게 다른지입니다. 특히 draft는 "검증 실패를 저장 전에 소진"하는 이 글의 설계 원칙과 직결되므로 우선순위를 높게 두길 권장합니다.
레퍼런스
- SAP Help Portal — RAP Save Sequence
- SAP Help Portal — RAP Additional Save / Unmanaged Save
- SAP Help Portal — RAP과 SAP LUW
- ABAP Keyword Documentation — Behavior Saver Class (cl_abap_behavior_saver)
- OASIS OData V4.01 Protocol — Batch Requests와 changeset 원자성
- SAP-samples — ABAP Flight Reference Scenario (RAP 레퍼런스 구현)
댓글 0
아직 댓글이 없습니다.