이 글에서 얻어갈 것 · 동시 편집 충돌 시나리오 정리
SAP S/4HANA Cloud 또는 ABAP Cloud 기반의 RAP(ABAP RESTful Application Programming) 환경에서 여러 사용자가 동일한 비즈니스 객체를 동시에 수정하려고 할 때 발생하는 데이터 정합성 문제는 실무에서 가장 빈번하게 발생하는 이슈 중 하나입니다. 본 글에서는 SalesOrder(헤더)와 SalesOrderItem(아이템) 계층 구조를 가지는 Managed BO를 예시로 들어, Lock Master/Lock Dependent 설계, ETag 기반 낙관적 잠금, 그리고 Behavior Definition 작성 패턴을 단계별로 다룹니다.
- Lock Master / Lock Dependent 개념과 부모-자식 잠금 전파 원리 이해
- ETag(Master / Dependent) 선언으로 Stale Update 방지하는 방법 습득
- Behavior Definition(BDEF)에 lock master 구문을 정확한 위치에 배치하는 패턴 학습
- Draft 활성화 시나리오에서의 추가 고려사항 점검
- 실무에서 자주 발생하는 CX_ABAP_BEHV_NO_LOCK 류 예외 트러블슈팅
이 글을 읽기 전 알아두면 좋은 사전 지식
본 글은 ABAP RAP의 기본 구성요소(CDS View Entity, Behavior Definition, Behavior Implementation, Service Definition, Service Binding)를 이미 한 번이라도 다뤄본 독자를 대상으로 합니다. Managed BO와 Unmanaged BO의 구분, 그리고 association을 이용한 Composition 관계 정의를 이해하고 있어야 합니다. OData V4 프로토콜의 If-Match 헤더 동작과 HTTP 412 Precondition Failed 응답 의미를 알면 ETag 절을 읽을 때 훨씬 수월합니다. Fiori Elements 또는 SAP Build Work Zone에서 List Report / Object Page 패턴으로 UI를 한 번이라도 띄워본 경험이 있으면 실습 검증에 유리합니다.
실습 환경과 사용 버전 점검
본 예제는 ABAP Cloud(Steampunk) 및 SAP BTP, ABAP environment 2025 릴리즈 기준으로 작성했습니다. 온프레미스의 경우 S/4HANA 2022 이상에서 Managed Scenario를 권장하며, ADT(ABAP Development Tools) 3.40 이상의 Eclipse 플러그인이 필요합니다. 패키지는 ZLOCAL 계열의 Software Component에 속한 패키지를 사용하고, Release Contract는 일반적으로 Use System-Internally를 선택합니다.
- Eclipse + ADT (Photon 이후 권장)
- ABAP Cloud Project 또는 ABAP On-Premise Project 연결
- BTP, ABAP environment의 Communication Arrangement(SAP_COM_0510 등)는 본 예제에서 직접 필요하지 않으나 Service Binding 테스트 시 Business Catalog 권한 필요
- 실습용 테이블 zsord_hdr_tbl, zsord_itm_tbl 두 개를 생성 (헤더-아이템 1:N 관계)
- Sample Data는 ABAP 단위 테스트 클래스 또는 SE16N(온프레미스)로 직접 인서트
테이블 키 설계는 헤더는 sales_order(CHAR10), 아이템은 sales_order + item_pos(NUMC6) 합성 키로 구성합니다. 두 테이블 모두 last_changed_at(타임스탬프) 필드를 가져야 ETag 처리가 자연스럽게 이어집니다.
Lock Master vs Lock Dependent — 비유로 풀어보는 동작 원리
RAP의 잠금 모델을 가장 쉽게 비유하면 '아파트 단지 출입 게이트'와 '각 호수의 현관문'의 관계입니다. SalesOrder가 단지 게이트(Lock Master)라면 SalesOrderItem들은 그 안의 각 호수(Lock Dependent)에 해당합니다. 누군가가 101호 현관(아이템)에 들어가려면 먼저 단지 게이트(헤더)에 들어가야 하고, 게이트가 잠겨 있다면 어떤 호수에도 진입할 수 없습니다. RAP은 자식 엔티티에 대한 변경 요청이 들어오면 자동으로 부모(Lock Master)의 잠금을 먼저 시도합니다. 이 전파(propagation)는 BDEF에서 'lock dependent by _ParentAssoc'을 선언했을 때 RAP 런타임이 알아서 처리합니다.
여기에 더해 RAP은 두 가지 잠금 패러다임을 동시에 활용합니다. 첫째는 비관적 잠금(Pessimistic Locking)으로, 변경 작업이 시작되는 순간 DB 락 매니저(SAP Lock Server, ENQUEUE)에 락 엔트리를 생성합니다. 둘째는 낙관적 잠금(Optimistic Locking)인데, ETag 필드(보통 last_changed_at)를 응답에 포함시키고 후속 PATCH/PUT 요청에서 If-Match 헤더로 다시 보내게 함으로써 '내가 읽은 이후 다른 사람이 바꿨는가?'를 검증합니다. 두 방식은 보완 관계입니다. 비관적 잠금은 진행 중인 사용자 간 충돌을 막고, 낙관적 잠금은 짧은 트랜잭션 또는 stateless API 호출 사이의 stale write를 막습니다.
핵심 정리: 비관적 잠금은 '동시에 누군가 편집 중인가'를, 낙관적 잠금은 '내가 가져간 데이터가 아직 최신인가'를 검증한다. RAP은 두 가지를 모두 활용하므로 BDEF에 lock master, etag master, etag dependent를 정확히 선언해야 한다.
주의할 점은 Composition으로 묶인 자식 엔티티는 자체적으로 Lock Master를 선언할 수 없다는 것입니다. 잠금은 항상 Composition Root에서만 마스터로 보유 가능하며, 자식들은 'lock dependent by <_Parent>'로 부모와의 연결만 명시합니다. 만약 SalesOrderItem이 자체 ENQUEUE를 잡으려 하면 RAP 활성화 검사에서 즉시 오류가 납니다.
1단계 — 최소 단위의 Lock Master 선언으로 시작
먼저 CDS View Entity와 Behavior Definition을 최소 형태로 작성합니다. 헤더 뷰만 만들고 lock master, etag master를 선언한 상태입니다.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order Header Root'
define root view entity ZR_SalesOrderTP
as select from zsord_hdr_tbl
{
key sales_order as SalesOrder,
customer_id as CustomerId,
total_amount as TotalAmount,
currency_code as CurrencyCode,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
@Semantics.systemDate.localInstanceLastChangedAt: true
local_last_changed_at as LocalLastChangedAt
}
managed implementation in class zbp_r_salesordertp unique;
strict ( 2 );
define behavior for ZR_SalesOrderTP alias SalesOrder
persistent table zsord_hdr_tbl
lock master
authorization master ( instance )
etag master LocalLastChangedAt
{
field ( readonly ) SalesOrder;
field ( numbering : managed ) SalesOrder;
create;
update;
delete;
mapping for zsord_hdr_tbl
{
SalesOrder = sales_order;
CustomerId = customer_id;
TotalAmount = total_amount;
CurrencyCode = currency_code;
LastChangedAt = last_changed_at;
LocalLastChangedAt = local_last_changed_at;
}
}
이 단계에서 중요한 포인트는 두 가지입니다. 첫째, 'lock master'는 BDEF의 헤더 절(strict 다음)에 위치해야 하며 alias 선언 뒤가 아닙니다. 둘째, etag master로 지정한 필드는 변경 추적 가능한 타임스탬프(utclong 또는 timestampl)여야 합니다. 만약 last_changed_at을 직접 노출했다가 Fiori UI가 자동 갱신할 수 없는 시점이 생기면 Stale ETag 문제가 발생하므로 LocalLastChangedAt(인스턴스 변경 시점) 사용이 일반적으로 권장됩니다.
2단계 — 자식 엔티티 결합과 실무 시나리오 보강
이제 SalesOrderItem을 추가하고 부모-자식 Composition을 묶습니다. 이 단계에서 등장하는 핵심 절은 'lock dependent by'와 'etag dependent by'입니다.
@AccessControl.authorizationCheck: #NOT_REQUIRED
define view entity ZR_SalesOrderItemTP
as select from zsord_itm_tbl
association to parent ZR_SalesOrderTP as _SalesOrder
on $projection.SalesOrder = _SalesOrder.SalesOrder
{
key sales_order as SalesOrder,
key item_pos as ItemPosition,
material_id as MaterialId,
quantity as Quantity,
net_price as NetPrice,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
_SalesOrder
}
define behavior for ZR_SalesOrderItemTP alias SalesOrderItem
persistent table zsord_itm_tbl
lock dependent by _SalesOrder
authorization dependent by _SalesOrder
etag dependent by _SalesOrder
{
update;
delete;
field ( readonly ) SalesOrder, ItemPosition;
association _SalesOrder;
mapping for zsord_itm_tbl
{
SalesOrder = sales_order;
ItemPosition = item_pos;
MaterialId = material_id;
Quantity = quantity;
NetPrice = net_price;
LastChangedAt = last_changed_at;
}
}
그리고 헤더 BDEF에 Composition 선언을 추가하여 두 엔티티를 묶습니다.
define behavior for ZR_SalesOrderTP alias SalesOrder
persistent table zsord_hdr_tbl
lock master
authorization master ( instance )
etag master LocalLastChangedAt
{
create;
update;
delete;
association _Items { create; with draft; }
...
}
실무에서 자주 빠뜨리는 부분은 'authorization dependent by'와 'etag dependent by'를 함께 선언하는 것입니다. 권한과 ETag도 부모로부터 상속받지 않으면 각 자식 호출마다 별도 권한 체크가 일어나거나, ETag 갱신이 부모와 동기화되지 않아 200/412가 불규칙하게 섞여 나옵니다. 로깅 측면에서는 Behavior Implementation 클래스의 modify 메서드에서 다음과 같이 변경 흐름을 남기는 것을 권장합니다.
METHOD update.
LOOP AT entities INTO DATA(ls_order).
" 실제 저장은 RAP 프레임워크가 처리하지만,
" 비즈니스 후처리(이벤트 발행 등)에 활용
cl_abap_tx=>save( ). " 직접 호출 금지 - 예시일 뿐
ENDLOOP.
" 실제 운영 코드에서는 application log 인스턴스에 기록
DATA(lo_log) = cl_bali_log_db_writer=>create_log( object = 'ZSORD' subobject = 'UPDATE' ).
ENDMETHOD.
3단계 — 프로덕션 강화: Draft, 성능, 테스트, 보안
운영 환경에서는 Draft 활성화가 일반적입니다. Draft를 켜면 활성(Active) 인스턴스와 Draft 인스턴스가 분리되어 저장되고, 잠금 전략도 다소 달라집니다. Active 인스턴스에 대해서는 짧은 시간 락만 유지하며, Draft에 대해서는 'exclusive lock'으로 단일 사용자 편집을 강제합니다.
managed implementation in class zbp_r_salesordertp unique;
strict ( 2 );
with draft;
define behavior for ZR_SalesOrderTP alias SalesOrder
persistent table zsord_hdr_tbl
draft table zsord_hdr_d
lock master total etag LocalLastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
{
field ( readonly ) SalesOrder;
field ( numbering : managed ) SalesOrder;
create;
update;
delete;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
association _Items { create; with draft; }
mapping for zsord_hdr_tbl
{
SalesOrder = sales_order;
CustomerId = customer_id;
TotalAmount = total_amount;
CurrencyCode = currency_code;
LocalLastChangedAt = local_last_changed_at;
}
}
'lock master total etag'는 Draft 시나리오에서 자식 변경까지 포함한 종합 ETag를 생성하도록 지시합니다. 이렇게 하면 사용자 A가 헤더만 변경하고, 사용자 B가 아이템만 변경하려고 시도해도 동일한 SalesOrder 인스턴스로 ETag 충돌이 감지됩니다. 'Activate optimized'는 활성화 시 변경된 필드만 검증해 활성 인스턴스 갱신 시간을 단축합니다.
성능 측면에서는 다음을 점검합니다.
- Lock Table 크기: 동시 편집자가 많을 경우 ENQUEUE 서버 부하 증가 — RZ12, SM12 모니터링
- Draft 정리: Draft 인스턴스가 누적되지 않도록 'discard after' 만료 정책 설정
- strict(2) 모드 유지: 컴파일러가 더 엄격하게 검증하므로 운영 결함 사전 차단
테스트는 ABAP Unit과 RAP TDF(Test Double Framework)를 조합합니다. 잠금 충돌은 시뮬레이션이 어려우므로 다음과 같이 EML(Entity Manipulation Language)로 2회 연속 UPDATE를 시도해 두 번째 호출이 실패하는지 확인합니다.
CLASS ltc_lock_conflict IMPLEMENTATION.
METHOD test_concurrent_update.
DATA failed TYPE RESPONSE FOR FAILED LATE ZR_SalesOrderTP.
DATA reported TYPE RESPONSE FOR REPORTED LATE ZR_SalesOrderTP.
MODIFY ENTITIES OF ZR_SalesOrderTP
ENTITY SalesOrder
UPDATE FIELDS ( CustomerId )
WITH VALUE #( ( SalesOrder = '0000000010'
CustomerId = 'C-NEW-01'
%control-CustomerId = if_abap_behv=>mk-on ) )
FAILED failed
REPORTED reported.
cl_abap_unit_assert=>assert_initial( failed-salesorder ).
ENDMETHOD.
ENDCLASS.
보안 관점에서는 'authorization master ( instance )' 외에 'authorization master ( global, instance )'를 검토합니다. Global Authorization은 작업 자체(예: Create)에 대한 권한을, Instance Authorization은 특정 인스턴스 단위 접근 권한을 체크합니다. 두 가지를 함께 선언하면 권한 매트릭스가 명확해집니다.
실수 모음과 자주 묻는 질문
현장에서 가장 자주 마주치는 오류 패턴은 다음과 같습니다.
- BDEF 활성화 시 'Lock master not allowed for non-root entity' — 자식 엔티티 BDEF에 lock master를 잘못 선언한 경우입니다. 자식은 항상 lock dependent by _Parent로 작성해야 합니다.
- 'CX_ABAP_BEHV_NO_LOCK' 런타임 예외 — Composition association을 BDEF에 선언하지 않았거나, Mapping에서 부모 키 필드를 매핑하지 않은 경우 발생합니다.
- '412 Precondition Failed'가 정상 호출에서도 뜸 — ETag 필드를 등록은 했지만 update 직후 last_changed_at이 갱신되지 않아 클라이언트가 받은 ETag와 서버 값이 어긋난 상황입니다. @Semantics.systemDateTime.lastChangedAt 어노테이션을 CDS 측에 반드시 부여해야 합니다.
Q1. Lock Master와 ETag Master를 둘 다 선언해야 하나요?
일반적으로 둘 다 선언하는 것을 권장합니다. Lock Master는 진행 중 편집자 간 충돌을, ETag Master는 stateless 호출 간 충돌을 막아 역할이 다릅니다.
Q2. Draft를 사용하면 Lock Master는 무의미한가요?
그렇지 않습니다. Draft 모드에서도 Active 인스턴스의 활성화(Save) 시점에는 짧지만 비관적 락이 필요하며, 동일 사용자 다른 세션 간 충돌 차단에도 활용됩니다.
Q3. Unmanaged BO에서는 어떻게 다른가요?
Unmanaged 시나리오에서는 lock master 절은 동일하게 선언하되, 실제 ENQUEUE/DEQUEUE 호출을 Behavior Implementation 클래스의 lock 메서드에서 직접 작성해야 합니다. Managed 모드는 RAP이 자동으로 처리합니다.
Q4. 'lock master total etag'와 일반 'etag master'의 차이는?
total etag는 자식 인스턴스 변경까지 ETag 산정에 포함시킵니다. 헤더-아이템 모두를 한 묶음으로 다루는 Object Page 시나리오에서 권장됩니다.
이어서 살펴볼 만한 주제와 연관 키워드
본 글에서 다룬 Lock Master/Dependent 구조에 익숙해졌다면, RAP의 추가 잠금 옵션인 'lock master total etag', 'lock dependent ( exclusive_locked )' 같은 세부 변형을 살펴보길 권장합니다. 또한 RAP BO Contract(Managed vs Unmanaged vs Managed with Unmanaged Save), Side Effects 어노테이션(@ObjectModel.text.element)을 활용한 자동 갱신 트리거, 그리고 Behavior Projection을 통한 외부 노출용 BO 분리 패턴이 다음 학습 단계로 자연스럽게 이어집니다. Fiori Elements 측에서는 List Report의 'Mass Edit' 시나리오에서 Lock 동작이 어떻게 직렬화되는지를 확인해보면 실무 적용 감각이 빠르게 향상됩니다.
더 깊이 파고들기 좋은 자료 모음
댓글 0
아직 댓글이 없습니다.
💬 댓글 작성, 좋아요, 북마크는 UI5 모드에서 사용할 수 있습니다.
UI5 모드에서 사용하기