RAP

발주 입력이 날아간다? Draft 활성화로 해결 #shorts #SAP #RAP

▶ YouTube에서 보기

Draft란 무엇인가 — RAP에서의 역할과 필요성

Fiori 기반 엔터프라이즈 애플리케이션에서 사용자가 긴 양식을 작성하다가 브라우저를 닫거나 세션이 끊겼을 때 입력값이 모두 사라진다면 어떨까요? Draft는 바로 이 문제를 해결하기 위해 ABAP RESTful Application Programming Model(RAP)에 도입된 핵심 기능입니다. 사용자가 "저장(Save)" 버튼을 누르기 전이라도 입력 중인 데이터를 서버 측 Draft 테이블에 자동으로 임시 저장하여, 다음 접속 시 이어서 작업할 수 있게 만들어줍니다.

다음 사항을 이 글에서 다룹니다.

  • Draft의 개념과 RAP 비즈니스 객체(Business Object) 내 동작 원리
  • CDS View와 Behavior Definition에서 Draft를 활성화하는 방법
  • ABAP 핸들러 클래스에서 draftAction과 validation을 구현하는 패턴
  • Fiori Elements List Report에서 Draft 상태가 표현되는 방식
  • 운영 환경에서 자주 발생하는 락(Lock)·고아(orphan) Draft 처리 전략

이 글을 읽기 전 알아두면 좋은 것

RAP의 기본 구성요소(Data Model, Behavior, Service Definition, Service Binding)에 대한 이해가 필요합니다. Managed/Unmanaged 시나리오 구분, CDS View Entity 작성 경험, ABAP 클래스 핸들러 메서드(FOR MODIFY, FOR READ) 작성 경험이 있다면 도움이 됩니다. Fiori Elements 기반 Preview 실행 경험이 있으면 마지막 섹션을 빠르게 따라올 수 있습니다.

버전·환경 및 사전 준비물

이 글의 예제는 SAP S/4HANA 2022 On-Premise 또는 SAP BTP ABAP Environment(Steampunk) 기반에서 검증된 문법을 사용합니다. ABAP Development Tools(ADT) 3.34 이상, ABAP Platform 2021 이상이 권장됩니다. 구버전(7.52 이하)에서는 Draft 관련 키워드 일부가 지원되지 않으므로 주의해야 합니다.

  • SAP ABAP Platform 2021 SP01 이상 (Steampunk 또는 S/4HANA On-Premise)
  • ADT(Eclipse) 3.34 이상 — CDS, BDEF, Service Binding 편집기 포함
  • 개발용 패키지 및 Transport Request
  • SAP Gateway 활성화 (S/4HANA의 경우 OData V4 노출용)
  • Fiori Elements Preview 권한(/ui2/flp 또는 Service Binding의 Web IDE Preview)

BTP ABAP Environment를 사용한다면 RAP Generator(ADT 마법사)를 통해 기본 골격을 생성해두면 작업이 훨씬 빨라집니다. 일반적으로 Draft는 Managed 시나리오에서 가장 매끄럽게 동작합니다.

Draft 아키텍처와 내부 동작 원리

Draft를 쉽게 비유하자면 "구글 문서의 자동 저장"과 닮았습니다. 사용자가 글을 쓰는 동안 명시적으로 Ctrl+S를 누르지 않아도, 시스템이 일정 주기로 임시본을 백엔드에 보관합니다. RAP에서는 이를 위해 두 개의 데이터 영역을 사용합니다.

  • Active 영역: 최종 승인되어 비즈니스 트랜잭션에 반영된 데이터. 정식 DB 테이블에 저장됩니다.
  • Draft 영역: 사용자가 편집 중인 임시 데이터. Draft Shadow Table(접두어가 보통 D로 시작)에 보관됩니다.

OData V4 레이어에서는 Draft 처리를 위해 표준 액션 Edit, Activate, Discard, Resume를 정의합니다. Fiori Elements는 이 표준 액션을 자동으로 호출하므로, 백엔드에서 Behavior Definition에 with draft 키워드만 추가해도 UI 흐름이 자연스럽게 연결됩니다.

내부적으로 RAP 런타임은 Draft 키로 GUID(DraftUUID)를 생성합니다. 이 UUID는 Active 인스턴스와 1:1로 매핑되며, 신규 생성 시에는 Active 인스턴스가 존재하지 않으므로 Draft만 존재하는 상태가 됩니다. 사용자가 Activate를 누르는 순간 Draft 레코드를 기준으로 Active 인스턴스가 INSERT 또는 UPDATE되고, Draft는 제거됩니다.

구버전 BOPF Draft와 비교하면, RAP Draft는 코드 자동 생성량이 훨씬 적고, OData V4 Draft Spec과 정합성이 높습니다. 다만 BOPF 기반 레거시 객체와 혼합 사용 시에는 Transaction 동기화 시점에 주의가 필요합니다.

실전 시나리오: 구매 발주(PurchaseOrder) 입력 흐름

예제로는 구매팀 담당자가 신규 발주서를 작성하는 시나리오를 다룹니다. 발주서는 헤더(공급사, 발주일, 통화)와 다수의 라인 아이템(품목, 수량, 단가)으로 구성되며, 사용자는 외부 회의에 다녀오는 동안 절반쯤 입력된 발주를 이어서 작업합니다. 데이터베이스 테이블은 다음과 같이 가정합니다.

@EndUserText.label : 'Purchase Order Header'
define table zpo_hdr_demo {
  key client          : mandt not null;
  key purchase_order  : zde_po_id not null;
  supplier_id         : zde_supplier_id;
  order_date          : datum;
  currency_code       : waers;
  total_amount        : dec15_2;
  overall_status      : char1;
  created_by          : syuname;
  created_at          : timestampl;
  last_changed_by     : syuname;
  last_changed_at     : timestampl;
  local_last_changed_at : timestampl;
}

CDS View Entity와 Draft 메타 설정

먼저 Active 데이터를 노출할 Root View Entity를 작성합니다. Draft 활성화의 핵심은 BDEF의 with draft 조합이며, CDS 측에서는 비즈니스 키만 명확히 표시해두면 됩니다.

@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'Purchase Order - Root'
define root view entity ZR_PurchaseOrderTP
  as select from zpo_hdr_demo
{
  key purchase_order        as PurchaseOrder,
      supplier_id           as SupplierId,
      order_date            as OrderDate,
      currency_code         as CurrencyCode,
      total_amount          as TotalAmount,
      overall_status        as OverallStatus,
      @Semantics.user.createdBy: true
      created_by            as CreatedBy,
      @Semantics.systemDateTime.createdAt: true
      created_at            as CreatedAt,
      @Semantics.user.lastChangedBy: true
      last_changed_by       as LastChangedBy,
      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at       as LastChangedAt,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at as LocalLastChangedAt
}

Projection View(ZC_PurchaseOrderTP)를 별도로 만들어 UI 노출용으로 활용합니다.

Behavior Definition에 Draft 활성화 및 검증 추가

Managed 시나리오에서 Draft를 켜는 가장 단순한 방법은 with draft 한 줄을 추가하는 것입니다.

managed implementation in class zbp_r_purchaseordertp unique;
strict ( 2 );
with draft;

define behavior for ZR_PurchaseOrderTP alias PurchaseOrder
persistent table zpo_hdr_demo
draft table zpo_hdr_demo_d
lock master
total etag LocalLastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
{
  field ( readonly ) PurchaseOrder, CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
  field ( mandatory ) SupplierId, OrderDate, CurrencyCode;

  create; update; delete;

  draft action Edit;
  draft action Activate optimized;
  draft action Discard;
  draft action Resume;
  draft determine action Prepare {
    validation validateSupplier;
    validation validateAmount;
  }

  validation validateSupplier on save { create; field SupplierId; }
  validation validateAmount   on save { create; update; field TotalAmount; }

  mapping for zpo_hdr_demo {
    PurchaseOrder = purchase_order;
    SupplierId    = supplier_id;
    OrderDate     = order_date;
    CurrencyCode  = currency_code;
    TotalAmount   = total_amount;
    OverallStatus = overall_status;
  }
}

draft table은 Shadow 테이블 이름을 명시합니다. Activate optimized는 변경분만 Active로 반영합니다. draft determine action Prepare는 Save 직전에 검증을 묶어 실행합니다.

핸들러 클래스에서 검증 로직 작성

검증은 Active와 Draft 모두에서 호출됩니다. 공급사 코드와 금액 검증 예시입니다.

CLASS lhc_purchaseorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS validateSupplier FOR VALIDATE ON SAVE
      IMPORTING keys FOR PurchaseOrder~validateSupplier.
    METHODS validateAmount FOR VALIDATE ON SAVE
      IMPORTING keys FOR PurchaseOrder~validateAmount.
ENDCLASS.

CLASS lhc_purchaseorder IMPLEMENTATION.
  METHOD validateSupplier.
    READ ENTITIES OF ZR_PurchaseOrderTP IN LOCAL MODE
      ENTITY PurchaseOrder FIELDS ( SupplierId )
      WITH CORRESPONDING #( keys )
      RESULT DATA(orders).

    LOOP AT orders INTO DATA(order).
      IF order-SupplierId IS INITIAL.
        APPEND VALUE #( %tky = order-%tky ) TO failed-purchaseorder.
        APPEND VALUE #( %tky = order-%tky
                        %msg = new_message( id = 'ZPO_MSG' number = '001'
                                 severity = if_abap_behv_message=>severity-error
                                 v1 = order-PurchaseOrder )
                        %element-SupplierId = if_abap_behv=>mk-on
                      ) TO reported-purchaseorder.
        CONTINUE.
      ENDIF.
      SELECT SINGLE supplier_id FROM zpo_supplier_demo
        WHERE supplier_id = @order-SupplierId INTO @DATA(lv_check).
      IF sy-subrc <> 0.
        APPEND VALUE #( %tky = order-%tky ) TO failed-purchaseorder.
        APPEND VALUE #( %tky = order-%tky
                        %msg = new_message( id = 'ZPO_MSG' number = '002'
                                 severity = if_abap_behv_message=>severity-error
                                 v1 = order-SupplierId )
                        %element-SupplierId = if_abap_behv=>mk-on
                      ) TO reported-purchaseorder.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD validateAmount.
    READ ENTITIES OF ZR_PurchaseOrderTP IN LOCAL MODE
      ENTITY PurchaseOrder FIELDS ( TotalAmount CurrencyCode )
      WITH CORRESPONDING #( keys )
      RESULT DATA(orders).

    LOOP AT orders INTO DATA(order).
      IF order-TotalAmount <= 0.
        APPEND VALUE #( %tky = order-%tky ) TO failed-purchaseorder.
        APPEND VALUE #( %tky = order-%tky
                        %msg = new_message( id = 'ZPO_MSG' number = '003'
                                 severity = if_abap_behv_message=>severity-error )
                        %element-TotalAmount = if_abap_behv=>mk-on
                      ) TO reported-purchaseorder.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

Behavior Projection과 Service Binding 연결

Projection BDEF에서 use draft를 선언해야 UI에서 Draft 기능이 활성화됩니다.

projection;
strict ( 2 );
use draft;

define behavior for ZC_PurchaseOrderTP alias PurchaseOrder
{
  use create; use update; use delete;
  use action Edit;
  use action Activate;
  use action Discard;
  use action Resume;
  use action Prepare;
}

Service Binding은 OData V4 UI 타입으로 생성합니다. Preview 실행 시 List Report에 "Edit", "Save", "Discard Draft" 버튼이 자동 표시됩니다.

Fiori Elements에서 Draft 화면 동작 확인

List Report에서 "My Drafts" 필터가 자동 노출됩니다. "Create" 클릭 후 일부 필드를 입력하고 브라우저를 새로고침한 다음 List Report로 돌아오면 입력 중이던 항목이 "Draft" 상태로 남아 있습니다.

OData V4 수준에서 Edit 버튼 클릭 시 발생하는 요청입니다.

POST /sap/opu/odata4/.../PurchaseOrder('4500001234')/Edit
{ "PreserveChanges": true }

응답으로 IsActiveEntity=false인 Draft 인스턴스가 반환되며, 이후 PATCH 요청은 모두 Draft 영역에만 적용됩니다. Save 버튼은 Activate 액션을 통해 검증을 통과한 변경분을 Active로 승격합니다.

실무 적용 시 주의사항 및 트러블슈팅

Q1. Draft 락으로 다른 사용자가 동일 객체를 편집할 수 없습니다.
RAP Draft는 Pessimistic Lock을 사용합니다. 한 사용자가 Edit를 누른 순간 다른 사용자는 읽기만 가능합니다. 협업 시나리오라면 BDEF의 lock masterEnqueueMode를 검토하세요.

Q2. 작업을 마치지 않고 떠난 "고아 Draft"가 쌓입니다.
Draft는 자동 만료되지 않습니다. 표준 보고서 RSDRAFT_HOUSEKEEPING 또는 S/4HANA 잡 SAP_BC_DRAFT_HOUSEKEEPING을 주기 스케줄링하세요.

Q3. Activate 후 화면에 검증 메시지가 표시되지 않습니다.
reported 구조에 %element-FieldName = if_abap_behv=>mk-on이 누락된 경우입니다. Active 테이블에 필드를 추가했을 때 Shadow 테이블을 동기화하지 않으면 CX_RAP_QUERY_PROVIDER 예외가 발생하므로 ADT BDEF 편집기의 "Adjust Draft Table"을 활용하세요.

댓글 0

아직 댓글이 없습니다.