Draft 처리의 필요성과 이 글의 방향
SAP RAP(RESTful ABAP Programming Model)에서 Draft는 사용자가 편집 중인 데이터를 임시로 보관하고, 세션이 끊기더라도 이어서 작업할 수 있게 해주는 핵심 기능입니다. Fiori Elements의 Object Page에서 "Edit" 버튼을 누르면 자동으로 Draft 인스턴스가 생성되는데, 이 흐름을 제대로 이해하지 못하면 활성화(Activate) 시점에 데이터 정합성 오류가 빈번하게 발생합니다. 이 글에서는 SalesOrder 시나리오를 기반으로 Behavior Definition에서 Draft를 단계별로 활성화하고, Draft Table을 설계하며, 실무에서 자주 마주치는 lock conflict·discarded draft·determination 미실행 문제를 해결하는 방법을 다룹니다.
- Draft 테이블과 Active 테이블의 이중 저장 구조 이해
- Behavior Definition에서
with draft구문 및 액션 정의 - Determination on modify 시점 조정과 Side Effect 연동
- Draft 활성화 실패 시 로그 추적 및 복구 절차
먼저 익혀두어야 할 지식
RAP의 Managed / Unmanaged 시나리오 차이, CDS View Entity 문법, Behavior Definition의 기본 골격(create; update; delete;), Fiori Elements Object Page의 라이프사이클에 대한 이해가 필요합니다. Draft는 대부분 Managed 시나리오에서 다루므로, ABAP RESTful Application Programming Model의 Managed Business Object 생성 경험이 있으면 진행이 수월합니다.
환경 구성 및 사전 준비 사항
이 글의 예제는 다음 환경을 기준으로 작성되었습니다.
- ABAP Platform: SAP S/4HANA 2023 또는 ABAP Cloud (Steampunk) 2308 이상
- 개발 도구: ADT(ABAP Development Tools) for Eclipse 2023-12 이상
- UI 프레임워크: SAP Fiori Elements V4 (SAPUI5 1.120+ 권장)
- ABAP 언어 버전: ABAP for Cloud Development (ABAP Cloud) 또는 Standard ABAP
- 권한 오브젝트:
S_DEVELOP, 서비스 바인딩 배포 권한, Draft 테이블 생성 권한
Draft를 사용하려면 CDS Root View의 키에 @ObjectModel.semanticKey가 지정되어 있고, 별도의 Draft Persistence Table(예: zsordr_draft)이 준비되어 있어야 합니다. Draft 테이블의 필드는 Active 테이블과 동일한 필드 + Draft Admin 필드(Include Structure sych_bdl_draft_admin_inc)를 포함해야 합니다.
Draft가 실제로 동작하는 원리
Draft를 처음 접하면 "그냥 임시 저장 기능" 정도로 오해하기 쉽지만, RAP Draft는 Active Instance와 Draft Instance라는 두 개의 별도 인스턴스가 공존하는 구조를 갖습니다. 다음 비유가 이해에 도움이 됩니다.
Draft는 마치 워드 문서의 "변경 내용 추적" 모드와 같습니다. 원본(Active)은 그대로 두고, 편집 중인 사본(Draft)에서 변경을 누적한 뒤, 최종적으로 "변경 승인(Activate)" 시점에 원본을 덮어씁니다.
내부적으로 Draft 흐름은 다음 6단계로 진행됩니다.
- Edit Action: 사용자가 편집을 시작하면 프레임워크가
Edit액션을 호출, Active 인스턴스를 복사한 Draft가 생성됩니다. - Prepare Phase: Draft 인스턴스에 변경사항이 순차적으로 반영되고, Determination이 실행됩니다.
- Validate Phase: 저장 전
Validation이 트리거되어 데이터 정합성을 검사합니다. - Activate Action: Draft → Active 전환이 이뤄지고, Draft 레코드는 정리됩니다.
- Discard Action: 사용자가 편집을 취소하면 Draft만 삭제되고 Active는 유지됩니다.
- Resume Action: 다른 세션·다른 기기에서 편집을 이어받을 때 실행됩니다.
각 단계는 %control과 %is_draft 필드를 통해 구분되며, Handler Class 내부에서 이 두 필드를 잘못 다루면 "Draft가 저장은 되는데 Object Page에는 반영이 안 되는" 증상이 발생합니다. %is_draft는 00(Active), 01(Draft)의 두 값을 가지며, 모든 Draft 관련 EML(Entity Manipulation Language) 호출에서 필수로 지정해야 합니다.
또 하나 중요한 개념은 Exclusive Lock입니다. Draft가 생성되는 순간 해당 Business Key에 대해 배타적 락이 걸리며, 기본적으로 사용자당 하나의 편집 세션만 허용됩니다. 다른 사용자가 동일 인스턴스에 접근하면 "Object is locked by another user" 메시지가 뜹니다.
단계별 구현: SalesOrder Draft 활성화
1단계 — 기본 Behavior Definition 작성
먼저 Root Entity에 Draft를 선언하는 기본 골격을 살펴보겠습니다. 아래는 판매 오더(SalesOrder)를 관리하는 BDEF의 최소 예제입니다.
managed implementation in class zbp_r_salesorder_tp unique;
strict ( 2 );
with draft;
define behavior for ZR_SalesOrderTP alias SalesOrder
persistent table zsordr_active
draft table zsordr_draft
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master ( instance )
draft determine action Prepare
{
field ( readonly : update ) SalesOrderUUID, SalesOrderID;
field ( mandatory : create ) CustomerID, OrderDate;
create;
update;
delete;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare
{
validation validateCustomer;
validation validateAmount;
}
association _Item { create; with draft; }
}
핵심은 with draft 키워드와 draft table 지정입니다. optimized 옵션은 Activate 시 변경된 필드만 저장하도록 하여 성능을 개선합니다. etag master ... total etag 조합은 낙관적 잠금(Optimistic Concurrency)을 담당합니다.
2단계 — Determination과 Validation을 갖춘 실전 시나리오
실무에서는 Draft 저장 시점에 자동 계산(예: 총액 산출)이 필요하고, 활성화 전에 반드시 데이터 검증이 필요합니다. Handler Class에서 ON MODIFY Determination을 구현하고, 로깅을 곁들인 예제를 보겠습니다.
CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS calculateTotal FOR DETERMINE ON MODIFY
IMPORTING keys FOR SalesOrder~calculateTotal.
METHODS validateAmount FOR VALIDATE ON SAVE
IMPORTING keys FOR SalesOrder~validateAmount.
ENDCLASS.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD calculateTotal.
READ ENTITIES OF ZR_SalesOrderTP IN LOCAL MODE
ENTITY SalesOrder BY \_Item
FIELDS ( Quantity UnitPrice )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_items).
LOOP AT lt_items INTO DATA(ls_item) GROUP BY ls_item-SalesOrderUUID
ASSIGNING FIELD-SYMBOL(<fs_group>).
DATA(lv_total) = REDUCE #( INIT sum TYPE p LENGTH 15 DECIMALS 2
FOR <line> IN GROUP <fs_group>
NEXT sum = sum + <line>-Quantity * <line>-UnitPrice ).
MODIFY ENTITIES OF ZR_SalesOrderTP IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( TotalAmount )
WITH VALUE #( ( %tky = VALUE #( SalesOrderUUID = <fs_group>-SalesOrderUUID
%is_draft = <fs_group>-%is_draft )
TotalAmount = lv_total ) )
REPORTED DATA(ls_reported).
cl_ba_log=>write_info(
iv_object = 'ZRAP_DRAFT'
iv_text = |Total recalculated for { <fs_group>-SalesOrderUUID }: { lv_total }| ).
ENDLOOP.
ENDMETHOD.
METHOD validateAmount.
READ ENTITIES OF ZR_SalesOrderTP IN LOCAL MODE
ENTITY SalesOrder
FIELDS ( TotalAmount CustomerID )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
IF ls_order-TotalAmount <= 0.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-salesorder.
APPEND VALUE #( %tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Total amount must be greater than zero' )
%element-TotalAmount = if_abap_behv=>mk-on )
TO reported-salesorder.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
여기서 두 가지 포인트가 있습니다. 첫째, IN LOCAL MODE를 사용하여 권한 체크를 건너뛰고 내부 로직을 실행합니다. 둘째, %tky(Transactional Key)에는 반드시 %is_draft가 포함되어야 Draft 인스턴스에 정확히 반영됩니다.
3단계 — 프로덕션 품질: 성능·보안·테스트
운영 환경에서는 대량 라인 아이템 처리 시 Draft가 느려지는 문제가 자주 보고됩니다. 다음 세 가지를 함께 적용합니다.
" 1. Prepare 액션에서 Side Effect 최소화
draft determine action Prepare
{
determination calculateTotal on save; " on modify → on save로 조정
validation validateAmount;
}
" 2. 대량 데이터: %pky 기반 배치 처리
METHOD prepareActivation.
READ ENTITIES OF ZR_SalesOrderTP IN LOCAL MODE
ENTITY SalesOrder ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders)
FAILED failed
REPORTED reported.
" Side Effect: Fiori에서 자동 재조회를 위한 트리거
APPEND LINES OF VALUE t_rba_reported(
FOR ls IN lt_orders
( %tky = ls-%tky
%element-TotalAmount = if_abap_behv=>mk-on
%element-Status = if_abap_behv=>mk-on ) )
TO reported-salesorder.
ENDMETHOD.
보안 측면에서는 authorization master ( instance )를 선언한 뒤 FOR AUTHORIZATION Handler를 반드시 구현해야 합니다. Draft가 생성되는 시점에 인스턴스 권한을 재검증하지 않으면, 다른 사용자의 Draft가 조회될 위험이 있습니다.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR SalesOrder RESULT result.
METHOD get_instance_authorizations.
READ ENTITIES OF ZR_SalesOrderTP IN LOCAL MODE
ENTITY SalesOrder FIELDS ( SalesOrgID ) WITH CORRESPONDING #( keys )
RESULT DATA(lt_check).
LOOP AT lt_check INTO DATA(ls_check).
AUTHORITY-CHECK OBJECT 'Z_SORG'
ID 'SORG' FIELD ls_check-SalesOrgID
ID 'ACTVT' FIELD '02'.
APPEND VALUE #( %tky = ls_check-%tky
%update = COND #( WHEN sy-subrc = 0
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized ) )
TO result.
ENDLOOP.
ENDMETHOD.
테스트는 CL_CDS_TEST_ENVIRONMENT와 CL_ABAP_BEHV_TEST_ENVIRONMENT를 조합하여 Draft 상태의 인스턴스를 목킹할 수 있습니다. 테스트 시 %is_draft = '01'을 명시적으로 지정하여 Draft 케이스와 Active 케이스를 분리 검증하는 것이 권장됩니다.
자주 마주치는 오류와 해결 전략
FAQ 1. "Object is locked by another user" — 나 자신인데도 왜?
같은 사용자가 두 개의 브라우저 탭에서 동일 인스턴스를 열면 발생합니다. Draft 락은 세션 기반이 아닌 사용자 기반이므로 이전 Draft를 먼저 처리해야 합니다. 해결은 SAP Gateway 트랜잭션 /IWFND/CLEANUP_CACHE로 캐시 정리 후, DRAFT_ADMIN 테이블에서 잔존 레코드를 확인합니다. 근본 해결책은 BDEF에 lock master total etag LastChangedAt을 명시하여 락 소유권을 명확히 하는 것입니다.
FAQ 2. Determination이 실행되지 않는다
가장 흔한 원인은 determination ... on modify { field FieldA; }에서 트리거 필드가 실제 UI에서 편집되지 않는 경우입니다. 예를 들어 Quantity 변경 시 Total을 재계산하도록 걸었는데 Object Page의 _Item 서브 노드가 별도 엔티티라면 Root의 Determination은 트리거되지 않습니다. 이 경우 Item 엔티티에도 Determination을 걸거나, determine action Prepare에 포함시켜 Activate 직전에 반드시 실행되게 해야 합니다.
FAQ 3. Draft는 저장되는데 Activate 시 사라진다
Draft Table의 필드 정의가 Active Table과 불일치할 때 발생합니다. 특히 UUID 타입 대신 CHAR32로 잘못 선언하거나, Draft Admin Include(sych_bdl_draft_admin_inc)가 누락된 경우가 대표적입니다. Draft 테이블을 다시 활성화하고 DRAFT_HDL_UUID 필드 존재 여부를 확인하십시오. 또한 Handler에서 failed·reported를 채우지 않고 예외만 던지면 프레임워크가 Draft를 롤백해버립니다.
FAQ 4. Fiori에서 "Discard Draft" 팝업이 매번 뜬다
이는 Manifest에서 Draft 지원이 명시적으로 활성화되지 않았거나, Service Binding의 Local Version이 오래된 경우입니다. Service Binding을 다시 게시(Publish Locally)하고, manifest.json의 "draftMode"를 확인하세요.
확장 학습을 위한 관련 주제
Draft를 마스터한 뒤에는 Numbering(Early vs Late)과 Draft의 상호작용, Action과 Function을 Draft 모드에서 호출할 때의 락 처리, Draft with Additional Save를 통해 Unmanaged 저장 로직 삽입하기 등으로 확장할 수 있습니다. 또한 Draft 데이터의 정기 정리를 위한 CL_ABAP_BEHV_AUX=>draft_cleanup 배치 작업 스케줄링도 실무에서 반드시 필요한 주제입니다. SAP Cloud Application Programming Model(CAP)의 Draft와 비교 학습하면 아키텍처 이해가 한층 깊어집니다.
더 살펴볼 만한 자료
- SAP Help — RAP: Draft Handling
- SAP Help — Draft in ABAP Cloud
- SAP Help — Behavior Definition Syntax (with draft)
- SAP Developers — Enable Draft Capabilities of a RAP Business Object
- SAP Community Blogs — RAP Draft Best Practices
댓글 0
아직 댓글이 없습니다.