RAP

ETag 없이 RAP 저장? 데이터 덮어씌워진다 #shorts #SAP #RAP

▶ YouTube에서 보기

개요 — 이 글에서 다루는 것

두 명의 담당자가 같은 서비스 오더를 동시에 열어 편집하면 어떤 일이 벌어질까요? ETag가 설정되지 않은 RAP 비즈니스 오브젝트에서는 나중에 저장한 사람이 먼저 저장한 사람의 변경을 조용히 덮어씌웁니다. 오류도, 경고도 없습니다. 이 글은 SAP BTP ABAP Environment(Steampunk) 및 S/4HANA(2021 이상) 기준으로 RAP의 낙관적 잠금(Optimistic Lock)을 ETag 필드로 구현하고 충돌을 감지하는 방법을 단계별로 설명합니다.

  • 낙관적 잠금과 비관적 잠금의 역할 분담을 구분할 수 있다
  • etag master / etag dependent by 구문을 실제 BO에 적용할 수 있다
  • 412 Precondition Failed 응답의 의미와 클라이언트 측 처리 방법을 이해한다
  • Draft BO에서의 Total ETag 개념까지 확장할 수 있다

사전에 알고 있으면 좋은 내용

CDS 뷰 엔티티와 RAP Behavior Definition(BDEF)의 기본 구조, Managed 시나리오의 CRUD 동작, OData 프로토콜의 HTTP 헤더 개념을 알고 있다면 수월합니다. EML(Entity Manipulation Language) 문법은 예제 코드에 주석을 달아 두었으므로 처음 보더라도 따라올 수 있습니다.

환경과 버전, 준비물

이 글의 코드는 다음 환경에서 검증하는 것을 전제로 합니다.

  • SAP BTP ABAP Environment 또는 S/4HANA 2021 이상 (On-Premise ABAP Platform 2021+, Cloud는 상시 최신)
  • ADT(ABAP Development Tools for Eclipse) — BDEF 편집은 ADT에서만 가능
  • Managed RAP BO 하나 (예제에서는 서비스 오더 ZR_SVCORDER를 새로 만듭니다)
  • 테스트용 OData V4 UI 서비스 바인딩 또는 Postman/EML 테스트 클래스

일반적으로 Cloud 환경에서는 abap.timestampl 타입의 관리 필드를 쓰는 것이 권장되며, 이 글도 그 방식을 따릅니다.

핵심 개념 — 낙관적 잠금은 "버전표 붙은 서류함"이다

비유로 시작하겠습니다. 회사 서류함에서 계약서를 꺼낼 때마다 서류에 버전 스티커가 붙어 있다고 상상해 보세요. 서류를 반납(저장)할 때 담당자는 "내가 꺼낼 때 버전이 v7이었다"라고 신고합니다. 그런데 그 사이 누군가 서류를 고쳐 v8이 되어 있다면? 반납이 거부되고 "최신본을 다시 확인하세요"라는 안내를 받습니다. 이것이 낙관적 잠금이고, 버전 스티커가 바로 ETag(Entity Tag)입니다.

RAP에는 두 가지 잠금이 서로 다른 시간 축을 담당합니다.

구분비관적 잠금 (lock master)낙관적 잠금 (etag master)
보호 구간하나의 LUW/트랜잭션 내부읽기와 저장 사이의 긴 공백(무상태 통신)
동작 방식Enqueue 락으로 즉시 차단저장 시점에 버전 비교 후 거부
실패 응답Locked(다른 사용자가 편집 중)HTTP 412 Precondition Failed

OData는 무상태(stateless) 프로토콜입니다. 사용자가 Fiori 화면에서 데이터를 읽고 30분 뒤 저장 버튼을 누르는 동안 서버는 아무 락도 잡고 있지 않습니다. 이 공백을 메우는 유일한 장치가 ETag입니다. 동작 흐름은 이렇습니다.

  1. 클라이언트가 엔티티를 GET → 응답에 ETag 값(예: 변경 타임스탬프) 포함
  2. 클라이언트가 PATCH/DELETE 요청 시 If-Match 헤더에 그 ETag를 실어 보냄
  3. RAP 런타임이 DB의 현재 ETag 필드 값과 비교
  4. 불일치 → 412 반환, 변경 거부. 일치 → 저장 후 새 ETag 발급

ETag 필드가 BDEF에 선언되어 있지 않으면 4번 비교 자체가 일어나지 않아 Lost Update(갱신 유실)가 발생합니다. 이것이 제목의 "덮어씌워진다"의 정체입니다.

실전 코드 — 3단계로 구현하기

1단계: 기본 — ETag 필드 선언과 etag master

먼저 루트 뷰 엔티티에 관리형 변경 타임스탬프 필드를 두고 애노테이션을 답니다.

define root view entity ZR_SVCORDER
  as select from zsvc_order
{
  key svc_order_uuid        as SvcOrderUuid,
      order_no              as OrderNo,
      technician_id         as TechnicianId,
      order_status          as OrderStatus,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_changed_at      as LocalChangedAt,
      @Semantics.systemDateTime.lastChangedAt: true
      changed_at            as ChangedAt
}

다음으로 Behavior Definition에서 이 필드를 ETag 마스터로 지정합니다.

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

define behavior for ZR_SVCORDER alias SvcOrder
persistent table zsvc_order
lock master
etag master LocalChangedAt   // 낙관적 잠금의 기준 필드
{
  create;
  update;
  delete;
  field ( readonly, numbering : managed ) SvcOrderUuid;
  field ( readonly ) LocalChangedAt, ChangedAt;
}

핵심 포인트: localInstanceLastChangedAt 애노테이션이 붙은 필드는 Managed 런타임이 해당 인스턴스가 변경될 때마다 자동 갱신하므로, 개발자가 직접 값을 채울 필요가 없습니다. 이 한 줄(etag master)만으로 OData 응답에 ETag가 실리고 If-Match 검증이 활성화됩니다.

2단계: 실무 시나리오 — 자식 엔티티와 충돌 상황 재현

서비스 오더에 부품 아이템(자식 엔티티)이 있다면, 아이템은 자체 ETag를 갖지 않고 루트의 ETag를 따라가도록 etag dependent by를 씁니다.

define behavior for ZR_SVCORDERITEM alias SvcOrderItem
persistent table zsvc_item
lock dependent by _SvcOrder
etag dependent by _SvcOrder   // 부모의 ETag를 상속
{
  update;
  delete;
  field ( readonly ) SvcOrderUuid, ItemUuid;
  association _SvcOrder;
}

이제 충돌을 EML로 재현해 봅니다. 오래된 상태를 기반으로 수정하면 어떤 실패가 돌아오는지 로그로 확인하는 코드입니다.

MODIFY ENTITIES OF zr_svcorder
  ENTITY SvcOrder
    UPDATE FIELDS ( OrderStatus )
    WITH VALUE #( ( SvcOrderUuid = lv_uuid
                    OrderStatus  = 'DONE' ) )
  FAILED   DATA(failed)
  REPORTED DATA(reported).

LOOP AT failed-svcorder INTO DATA(fail_line).
  " %fail-cause = if_abap_behv=>cause-conflict 이면 ETag 충돌
  IF fail_line-%fail-cause = if_abap_behv=>cause-conflict.
    " 실무: 애플리케이션 로그(BALI)에 기록 후 사용자에게 재조회 유도
    lo_log->add_msg( iv_msgid = 'ZSVC' iv_msgno = '042'
                     iv_msgv1 = |{ fail_line-SvcOrderUuid }| ).
  ENDIF.
ENDLOOP.

OData 레이어에서는 동일 상황이 412 Precondition Failed로 나타납니다. Fiori Elements는 이를 받아 "다른 사용자가 데이터를 변경했습니다" 다이얼로그를 자동 표시하고 새로고침을 유도합니다. 커스텀 UI5 앱이라면 직접 처리해야 합니다.

// UI5 커스텀 앱 — OData V4 모델에서 412 처리
oContext.setProperty("OrderStatus", "DONE");
oModel.submitBatch("orderGroup").catch(function (oError) {
  if (oError.status === 412) {
    MessageBox.warning("다른 사용자가 먼저 저장했습니다. 최신 데이터를 다시 불러옵니다.");
    oContext.refresh(); // 새 ETag 재취득
  }
});

3단계: 프로덕션 — Draft·Total ETag·테스트

Draft를 켠 BO에서는 이야기가 한 단계 확장됩니다. Draft 문서 전체(루트+자식 전체)의 변경을 대표하는 Total ETag를 루트에 선언해야 하며, 인스턴스별 ETag와 별도로 관리됩니다.

define behavior for ZR_SVCORDER alias SvcOrder
persistent table zsvc_order
draft table zsvc_order_d
lock master total etag ChangedAt        // 문서 전체 대표 ETag
etag master LocalChangedAt              // 인스턴스 단위 ETag
{
  ...
}

구분이 중요합니다. LocalChangedAt(localInstanceLastChangedAt)은 해당 인스턴스가 바뀔 때만 갱신되고, ChangedAt(lastChangedAt)은 자식 아이템이 바뀌어도 루트에서 갱신됩니다. 그래서 일반적으로 인스턴스 ETag에는 전자를, Total ETag에는 후자를 쓰는 조합이 권장됩니다. 반대로 쓰면 아이템만 고친 경우 루트 ETag가 변하지 않아 충돌 감지가 누락될 수 있습니다.

배포 전에 ETag 동작을 회귀 테스트로 고정해 둡니다.

CLASS ltc_etag_conflict DEFINITION FINAL FOR TESTING
  RISK LEVEL HARMLESS DURATION SHORT.
  PRIVATE SECTION.
    METHODS stale_update_rejected FOR TESTING.
ENDCLASS.

CLASS ltc_etag_conflict IMPLEMENTATION.
  METHOD stale_update_rejected.
    " Given: 세션 A가 읽은 뒤 세션 B가 먼저 수정·커밋했다고 가정
    " (테스트 더블: CDS Test Double Framework로 DB 상태 시뮬레이션)
    " When: 세션 A가 과거 ETag로 UPDATE 시도
    " Then: failed-cause 는 conflict 이어야 한다
    cl_abap_unit_assert=>assert_equals(
      act = failed_line-%fail-cause
      exp = if_abap_behv=>cause-conflict ).
  ENDMETHOD.
ENDCLASS.

보안·성능 관점의 마무리 점검: ETag 필드는 반드시 field ( readonly )로 막아 클라이언트가 조작하지 못하게 하고, 타임스탬프는 timestampl(마이크로초 정밀도)을 사용해 짧은 간격의 연속 저장에서도 값이 충돌하지 않게 합니다. 초 단위 타임스탬프는 같은 초 안에 두 번 저장되면 감지에 실패할 수 있습니다.

흔한 실수와 트러블슈팅 FAQ

  • Q1. etag master를 선언했는데 충돌이 감지되지 않아요. ETag 필드에 애노테이션(@Semantics.systemDateTime.localInstanceLastChangedAt)이 빠져 값이 갱신되지 않는 경우가 가장 흔합니다. Managed 시나리오가 아니라면 save 단계에서 직접 필드를 갱신하는 로직이 있는지 확인하세요.
  • Q2. 아이템만 수정했는데 412가 안 떠요. 자식 BDEF에 etag dependent by _Parent가 없거나, Total ETag를 localInstanceLastChangedAt 필드로 잘못 지정한 사례입니다. 문서 전체를 대표하는 필드는 lastChangedAt 계열이어야 합니다.
  • Q3. lock master가 있으면 ETag는 필요 없지 않나요? 아닙니다. Enqueue 락은 요청이 서버에 머무는 짧은 구간만 보호합니다. 화면에서 읽고 저장하기까지의 긴 공백은 락이 풀린 상태이므로 ETag 없이는 Lost Update를 막을 수 없습니다. 둘은 대체재가 아니라 보완재입니다.
  • Q4. 412 이후 무조건 덮어쓰기 하고 싶다는 요구가 있어요. If-Match에 *를 보내면 검증을 건너뛸 수 있지만, 감사 추적이 필요한 업무 데이터에는 일반적으로 권장되지 않습니다. 최신본을 다시 읽어 병합하는 UX가 안전합니다.

여기서 더 나아가기

ETag로 충돌 "감지"를 마쳤다면, 다음 관심사는 충돌 "예방"과 사용자 경험입니다. Draft 기반 편집 세션과 Exclusive Lock의 상호작용, lock dependent 체인 설계, RAP BO의 Late Numbering과 ETag의 관계, 그리고 OData V4의 Prefer: handling=strict 처리 등을 이어서 살펴보면 동시성 설계가 완성됩니다. 커스텀 UI5 앱이라면 412 수신 시 diff 화면을 보여주는 병합 UX도 좋은 확장 주제입니다.

참고 링크 모음

댓글 0

아직 댓글이 없습니다.