RAP

RAP Draft 빠뜨리면 큰일 — ETag Lock 충돌 #shorts #SAP #RAP

▶ YouTube에서 보기

개요와 이 글에서 다루는 범위

SAP RAP(RESTful Application Programming Model)에서 draft 활성화는 Fiori Elements 기반 UX의 표준 권장 사항으로 자리잡았습니다. 그러나 실제 프로젝트에서는 라이선스 데이터 모델 제약, 외부 시스템 동기화, 짧은 트랜잭션 패턴 등 다양한 이유로 Draft를 사용하지 않는(without draft) 시나리오를 선택하는 경우가 많습니다. Draft 없이 개발하면 코드는 단순해지지만, 사용자 경험(UX)과 동시성 제어 측면에서 놓치기 쉬운 함정이 다수 존재합니다.

  • Draft 미사용 BO(Business Object)의 트랜잭션 흐름 이해
  • ETag(Entity Tag)와 Optimistic Concurrency Control 동작 원리
  • Pessimistic Lock과 Lock Master 설정 시 주의점
  • Fiori Elements UI에서 발생하는 UX 저하 패턴과 보완 기법
  • 실전 BDEF 코드와 ABAP 핸들러 클래스 구현 예제

이 글에서 다루는 기술 배경

이 글은 RAP의 기본 구조(BDEF, Behavior Implementation, Projection)와 CDS View Entity 작성 경험을 보유한 개발자를 대상으로 합니다. ABAP RESTful Programming Model의 managed/unmanaged 구분, EML(Entity Manipulation Language)을 이용한 BO 조작 경험이 있어야 합니다. 또한 Fiori Elements List Report/Object Page의 편집 라이프사이클(Edit → Save) 흐름을 인지하고 있어야 합니다.

개발 환경과 사전 준비물

이 글의 코드는 다음 환경을 기준으로 검증되었습니다. 사용 중인 시스템 버전에 따라 일부 어노테이션이나 키워드가 다를 수 있으므로 릴리스 노트를 함께 확인하는 것을 권장합니다.

  • SAP S/4HANA 2023 FPS01 또는 ABAP Cloud 2402 이상
  • ABAP Development Tools (ADT) for Eclipse 2024-03 릴리스 이상
  • SAP Fiori Elements V4 (UI5 1.120+)
  • 테스트용 백엔드: SAP BTP ABAP Environment(Steampunk) 또는 온프레미스 S/4HANA
  • 샘플 시나리오: SalesQuotation Business Object (헤더 + 아이템)
ABAP Cloud 환경에서는 Released API만 사용할 수 있으므로, 일부 클래식 Lock Object 호출이 제한될 수 있습니다. 이 경우 RAP가 제공하는 표준 Lock Master 메커니즘으로 대체해야 합니다.

Draft 없는 RAP의 동작 원리와 핵심 개념

Draft가 활성화된 BO는 사용자가 "Edit" 버튼을 누르는 순간 Draft 인스턴스가 데이터베이스에 생성되고, 사용자는 이 임시 사본을 자유롭게 수정한 뒤 "Save" 시점에 Active 인스턴스로 병합합니다. 반면 Draft 없는 BO는 이런 임시 영역이 없고, 사용자가 변경한 내용이 곧바로 트랜잭션 버퍼에 반영된 뒤 즉시 Save 처리됩니다.

이 차이는 두 가지 동시성 제어 모델로 이어집니다.

  • Pessimistic Locking: Edit 시점에 BO 전체에 락을 걸어 다른 사용자의 동시 편집을 차단합니다. Draft 없는 BO는 락 보유 시간이 짧지만, 사용자가 브라우저를 닫으면 락이 일정 시간 남아있을 수 있습니다.
  • Optimistic Concurrency Control (ETag): 락을 걸지 않고, Save 시점에 ETag를 비교해 충돌을 감지합니다. 데이터가 변경된 경우 412(Precondition Failed)를 반환하여 사용자가 다시 로드하도록 유도합니다.

비유하자면, Draft는 "구글 문서의 자동 저장 사본"이고, Without Draft + ETag는 "Git의 fast-forward 충돌 감지"에 가깝습니다. Draft 없는 RAP에서는 ETag가 Save 단계에서 변경 충돌을 잡아주는 유일한 안전망입니다.

또한 Lock Master는 컴포지션 트리의 루트 노드를 기준으로 락을 관리합니다. SalesQuotation 헤더가 루트이고 SalesQuotationItem이 자식인 경우, 아이템 편집 시에도 헤더 락을 잡아야 일관성이 유지됩니다.

실전 구현 1단계 — 기본 BDEF와 동작 검증

먼저 Draft 없는 managed BO의 가장 단순한 형태를 살펴보겠습니다. SalesQuotation이라는 견적 헤더에 표준 CRUD 동작과 ETag, Lock을 선언합니다.

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

define behavior for ZR_SalesQuotation alias Quotation
persistent table zsalesquot
lock master
authorization master ( instance )
etag master last_changed_at
{
  create;
  update;
  delete;

  field ( readonly ) quotation_id, created_at, created_by;
  field ( numbering : managed ) quotation_uuid;

  mapping for zsalesquot
  {
    QuotationUuid    = quotation_uuid;
    QuotationId      = quotation_id;
    CustomerId       = customer_id;
    TotalAmount      = total_amount;
    LastChangedAt    = last_changed_at;
  }
}

핵심은 etag master last_changed_at 구문입니다. 이 한 줄로 RAP 런타임은 모든 GET/PATCH 요청에서 last_changed_at 필드를 ETag로 사용합니다. lock master는 Edit 액션 시 자동 락을 의미합니다.

간단한 EML 테스트로 동작을 확인해 보겠습니다.

DATA(lt_update) = VALUE TABLE FOR UPDATE ZR_SalesQuotation(
  ( %tky-QuotationUuid = lv_uuid
    TotalAmount        = '15000.00'
    %control-TotalAmount = if_abap_behv=>mk-on ) ).

MODIFY ENTITIES OF ZR_SalesQuotation
  ENTITY Quotation
    UPDATE FIELDS ( TotalAmount )
    WITH lt_update
  FAILED   DATA(ls_failed)
  REPORTED DATA(ls_reported).

COMMIT ENTITIES.

실전 구현 2단계 — 동시성 충돌 처리와 로깅

실무에서는 두 사용자가 동일한 견적을 동시에 편집할 때를 반드시 고려해야 합니다. Draft가 있다면 Draft가 다른 사용자에게 보여 자연스럽게 회피되지만, Draft가 없는 경우 ETag 충돌이 곧바로 사용자에게 노출됩니다. 이때 충돌 메시지를 의미 있게 가공하고 로깅하는 것이 중요합니다.

CLASS lhc_quotation IMPLEMENTATION.

  METHOD update.
    " ETag 검증은 RAP 프레임워크가 자동 수행
    " 추가 비즈니스 검증 + 충돌 컨텍스트 로깅
    LOOP AT keys INTO DATA(ls_key).
      SELECT SINGLE last_changed_at, changed_by
        FROM zsalesquot
        WHERE quotation_uuid = @ls_key-QuotationUuid
        INTO @DATA(ls_db).

      IF sy-subrc = 0 AND ls_db-changed_by <> sy-uname.
        " 다른 사용자가 마지막으로 수정함을 감지
        zcl_quot_logger=>log_concurrent_edit(
          iv_uuid    = ls_key-QuotationUuid
          iv_other   = ls_db-changed_by
          iv_current = CONV #( sy-uname ) ).
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

ENDCLASS.

RAP는 412 응답을 자동 생성하지만, 사용자에게는 "데이터가 변경되었습니다. 새로고침하세요"라는 메시지만 표시됩니다. 누가 언제 바꿨는지 파악하려면 위처럼 별도 로깅이 필요합니다.

또한 Lock 충돌 시에는 다음과 같이 메시지를 커스터마이즈하는 것이 일반적으로 권장됩니다.

METHOD lock.
  TRY.
      DATA(lo_lock) = cl_abap_lock_object_factory=>get_instance( 'EZSALESQUOT' ).
      " 락 시도
    CATCH cx_abap_foreign_lock INTO DATA(lx_foreign).
      APPEND VALUE #(
        %tky    = keys[ 1 ]-%tky
        %msg    = new_message_with_text(
                    severity = if_abap_behv_message=>severity-error
                    text     = |견적이 사용자 { lx_foreign-user_name }에 의해 편집 중입니다| )
      ) TO reported-quotation.
  ENDTRY.
ENDMETHOD.

실전 구현 3단계 — 프로덕션 수준 패턴

운영 환경에서는 세 가지를 추가로 다뤄야 합니다. 첫째, ETag 필드는 반드시 자동 갱신되어야 합니다. last_changed_at을 수동으로 채우면 동일 트랜잭션 내에서 ETag가 갱신되지 않아 두 번째 Save가 412로 거부될 수 있습니다.

METHOD save_modified.
  IF update-quotation IS NOT INITIAL.
    LOOP AT update-quotation ASSIGNING FIELD-SYMBOL().
      -LastChangedAt = cl_abap_tstmp=>utclong2tstmp(
                                     utclong_current( ) ).
      -ChangedBy     = sy-uname.
    ENDLOOP.

    UPDATE zsalesquot FROM TABLE @( CORRESPONDING #( update-quotation
                                       MAPPING quotation_uuid = QuotationUuid
                                               last_changed_at = LastChangedAt
                                               changed_by      = ChangedBy ) ).
  ENDIF.
ENDMETHOD.

둘째, 권한 체크는 Edit 시작과 Save 시점에 모두 수행해야 합니다. Draft가 없으면 Edit 도중 권한이 회수되는 경우를 잡을 시점이 Save밖에 없기 때문입니다. authorization master ( instance )로 선언한 뒤 get_instance_authorizations 메서드에서 인스턴스 단위 권한을 명시적으로 확인하는 것이 안전합니다.

셋째, Fiori Elements UI에서는 @UI.lineItem의 inline edit를 끄고 Object Page에서만 편집하도록 강제하는 것이 좋습니다. List Report에서 inline edit를 허용하면 ETag 충돌 메시지가 행마다 발생해 UX가 무너집니다.

@Metadata.allowExtensions: true
@UI.headerInfo: { typeName: 'Quotation', typeNamePlural: 'Quotations' }
annotate view ZC_SalesQuotation with
{
  @UI.lineItem: [ { position: 10, importance: #HIGH } ]
  @UI.identification: [ { position: 10 } ]
  QuotationId;

  @UI.lineItem: [ { position: 20 } ]
  CustomerId;

  @UI.hidden: true
  LastChangedAt;
}

마지막으로 성능 측면에서, Lock Master는 BO 인스턴스당 1회만 잡히도록 캐싱되어야 합니다. 핸들러에서 반복적으로 ENQUEUE를 호출하면 락 테이블 부하가 커집니다. RAP 프레임워크가 자체 관리하는 락에 의존하고, 별도 ENQUEUE는 외부 시스템 연동 등 특수 케이스로 제한하는 것을 권장합니다.

자주 빠지는 함정과 해결 가이드

Q1. Save 후에도 "데이터가 변경되었습니다" 메시지가 반복적으로 나타납니다.
ETag 필드가 Save 시점에 갱신되지 않은 경우입니다. save_modified나 데이터베이스 트리거에서 last_changed_at을 반드시 새 타임스탬프로 업데이트해야 합니다. 또한 ETag 필드 타입이 timestampl(utclong)이 아니라 일반 dats/tims 조합이면 RAP가 변경을 감지하지 못할 수 있습니다.

Q2. 사용자가 브라우저를 닫으면 락이 풀리지 않습니다.
Pessimistic Lock은 세션이 비정상 종료되면 백엔드 lock entry가 일정 시간 남아 있을 수 있습니다. 이를 줄이려면 Edit 후 일정 시간 동안 활동이 없으면 클라이언트에서 자동으로 락을 해제하는 패턴을 적용하거나, SM12에서 만료 정책을 검토해야 합니다. 가능하다면 ETag 기반 Optimistic 모드로 전환하는 것이 사용자 경험에 더 우호적입니다.

Q3. Draft 없는 BO에서 Validation은 언제 실행되나요?
Draft가 없으므로 Validation은 Save 직전에만 한 번 실행됩니다. 사용자는 입력을 모두 마친 뒤에야 오류를 마주치게 됩니다. UX 개선을 위해 CDS 어노테이션의 @Assert.format이나 Side Effects를 사용해 클라이언트 단에서 즉시 피드백을 주도록 보완하는 것이 일반적으로 권장됩니다.

그 외 자주 나오는 문제

  • 컴포지션 자식 편집 시 부모 ETag가 갱신되지 않아 충돌 감지 누락 → 부모의 last_changed_at도 함께 갱신해야 합니다.
  • Action 호출이 ETag 검증을 우회 → etag 키워드를 action 정의에 명시적으로 추가합니다.
  • Backend Determination에서 변경한 필드가 ETag 갱신을 트리거하지 않음 → Determination 후 명시적으로 ETag 필드를 다시 계산해야 합니다.

심화 학습 방향

Draft 없는 시나리오를 더 깊이 다루려면 다음 주제를 이어서 학습하는 것을 권장합니다. 외부 시스템 동기화가 필요한 경우 unmanaged BO로 전환하면서 ETag/Lock을 직접 구현하는 패턴, 짧은 트랜잭션과 대규모 일괄 처리에 적합한 EML mass operation, 그리고 Side Effects를 활용한 클라이언트 즉시 검증 등이 있습니다. 또한 RAP BO를 Service Consumption Model로 호출하는 백엔드 간 통신 시 ETag 전달 규칙도 별도 학습이 필요합니다.

댓글 0

아직 댓글이 없습니다.