이 글에서 다루는 내용
ABAP RESTful Application Programming Model(RAP)에서 Draft Activate는 사용자가 작성 중인 데이터를 임시 저장(Draft)했다가 검증을 거쳐 정식 데이터(Active)로 전환하는 핵심 시나리오입니다. Fiori Elements의 Object Page에서 "Save Draft" → "Activate" 버튼이 이 흐름을 그대로 따릅니다. 이 글은 PurchaseOrder(구매 주문) 엔티티를 기반으로 Draft Activate 구현 시 실무에서 가장 자주 발생하는 3가지 실수를 재현하고, 정상 동작하는 코드를 단계적으로 완성합니다.
- BDEF에서
with draft선언과draft table매핑이 누락된 사례 진단 - EML(Entity Manipulation Language)
MODIFY ... OPERATION activate호출 흐름과 트랜잭션 버퍼 동기화 validation과determination on save의 실행 순서, Draft → Active 전이 시 검증 훅 누락 방지- Active/Draft 두 개의 영속 테이블 구조와 Key 처리(UUID 기반 draft key)
읽기 전에 갖춰야 할 배경
이 글은 ABAP CDS View Entity, Behavior Definition(BDEF), Behavior Implementation(BIL) 클래스의 기본 문법을 알고 있다는 전제로 진행합니다. Managed 시나리오와 EML MODIFY ENTITIES 구문, Fiori Elements Object Page의 Edit/Save 동작을 한 번 이상 경험해 본 사용자라면 무리 없이 따라올 수 있습니다. SAP Gateway OData V4 노출, Service Definition/Binding 생성 절차는 기본 수준으로만 다룹니다.
실습 환경과 준비물
이 글의 코드는 다음 환경을 기준으로 작성되었습니다.
- ABAP Platform: SAP S/4HANA 2023 또는 ABAP Cloud(Steampunk) 2402 이상 권장
- 개발 도구: ABAP Development Tools(ADT) for Eclipse 2024-03 이상
- RAP 시나리오: Managed with unmanaged save 또는 순수 Managed(저장 로직은 프레임워크 위임)
- UI 검증: Fiori Elements Preview(Service Binding의 Preview 기능) 또는 SAP Business Application Studio
- 데이터베이스: 활성(Active) 테이블
zpurord_a, 드래프트(Draft) 테이블zpurord_d두 개 사전 생성
Active 테이블과 동일한 필드 + Draft 관리용 표준 필드(draftuuid, draftentitycreationdatetime, draftentitylastchangedatetime, haspreservedchanges 등)를 가진 Draft 테이블이 반드시 별도로 존재해야 합니다. Include 구조 SYCH_BDL_DRAFT_ADMIN_INC를 활용하면 표준 필드를 일관되게 추가할 수 있습니다.
Draft 동작 원리와 핵심 개념
RAP의 Draft는 흔히 "임시 저장"으로 설명되지만 내부 구조는 더 정교합니다. 하나의 비즈니스 객체(BO)는 Active 테이블과 Draft 테이블이라는 두 개의 영속 영역을 가지며, 동일 키 또는 별도 키(UUID)를 통해 양쪽 레코드가 1:1로 매핑됩니다.
비유하자면, Active 테이블은 "출판된 문서"이고 Draft 테이블은 "작성 중인 원고"입니다. 사용자가 Object Page에서 Edit 버튼을 누르는 순간 프레임워크는 Active 레코드를 Draft 테이블로 복사(Edit Draft) 하고, 사용자가 입력하는 모든 변경은 Draft 측에만 기록됩니다. 이때 EML 명령 흐름은 다음과 같습니다.
- EDIT: Active → Draft 복제. Draft Admin 필드(생성/변경 일시, 사용자) 자동 채움
- UPDATE/MODIFY: 사용자가 필드 변경. Draft 테이블만 갱신
- PREPARE: Draft 상태에서 사전 검증 실행(저장 전 일관성 체크)
- ACTIVATE: Draft → Active 전환.
determination on save,validation on save호출 후 Active 테이블에 반영, Draft 레코드 삭제
여기서 핵심은 validate가 두 번 호출될 수 있다는 점입니다. Prepare 단계에서 한 번(트리거: 사용자 명시적 Prepare 또는 Activate 직전), Activate 단계에서 또 한 번. validation을 BDEF에 선언만 하고 on save 트리거를 지정하지 않으면 Draft → Active 전이 시점에 호출되지 않아 잘못된 데이터가 그대로 활성화됩니다.
또 하나의 함정은 Draft Key입니다. Active 키는 보통 의미 있는 키(예: 구매 주문 번호)이지만 Draft 단계에서는 아직 번호 채번이 안 되었을 수 있으므로, draftuuid(16바이트 RAW)를 별도 키로 사용해 Draft 레코드를 식별합니다. BDEF에서 with draft 선언 시 이 매핑을 명시하지 않으면 ACTIVATE 호출이 "draft entity not found"로 실패합니다.
1단계 — 기본 BDEF와 Active/Draft 테이블 매핑
먼저 가장 단순한 Managed 시나리오로 Draft를 활성화합니다. PurchaseOrder 엔티티의 BDEF는 다음과 같이 작성합니다.
managed implementation in class zbp_r_purord_001 unique;
strict ( 2 );
with draft;
define behavior for ZR_PurchaseOrder001 alias PurchaseOrder
persistent table zpurord_a
draft table zpurord_d
lock master
total etag LastChangedAt
authorization master ( instance )
etag master LastChangedAt
{
field ( readonly ) PoUuid, PoNumber, CreatedAt, CreatedBy;
field ( mandatory ) Supplier, OrderDate;
create;
update;
delete;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare
{
validation validateSupplier;
validation validateOrderDate;
}
validation validateSupplier on save
{ create; update; field Supplier; }
validation validateOrderDate on save
{ create; update; field OrderDate; }
mapping for zpurord_a
{
PoUuid = po_uuid;
PoNumber = po_number;
Supplier = supplier_id;
OrderDate = order_date;
TotalAmount = total_amount;
LastChangedAt = last_changed_at;
}
}
이 단계에서 가장 흔히 놓치는 부분은 with draft 선언과 draft table zpurord_d 라인입니다. 둘 중 하나라도 빠지면 BDEF는 활성화되지만 Fiori UI에서 Edit 버튼이 나타나지 않거나, Activate 호출이 런타임 덤프로 이어집니다. 또한 draft action Activate optimized의 optimized 옵션은 Active 테이블 키와 Draft 키가 동일할 때 추가 검증 단계를 생략해 성능을 높여 줍니다.
2단계 — EML로 Draft Activate를 프로그램에서 직접 호출
UI가 아닌 백엔드 배치/이벤트 로직에서 Draft를 활성화해야 하는 경우가 있습니다. 예를 들어 야간 배치가 임시 저장 상태로 남은 PO를 일괄 활성화하거나, 외부 시스템 IDoc 수신 후 Draft를 만들고 검증 통과 시 자동 Activate 하는 시나리오입니다. 이때 EML MODIFY ENTITIES ... OPERATION /BOBF/IF_FRW_C=>SC_OPERATION-ACTIVATE 가 아니라 RAP 표준 구문을 사용합니다.
METHOD activate_drafts_in_bulk.
DATA lt_drafts TYPE TABLE FOR ACTION IMPORT
zr_purchaseorder001\Activate.
DATA lt_failed TYPE TABLE FOR FAILED zr_purchaseorder001.
DATA lt_report TYPE TABLE FOR REPORTED zr_purchaseorder001.
" 1) 활성화 대상 Draft UUID 수집
SELECT po_uuid FROM zpurord_d
WHERE haspreservedchanges = @abap_false
INTO TABLE @DATA(lt_target).
LOOP AT lt_target INTO DATA(ls_target).
APPEND VALUE #( %key-PoUuid = ls_target-po_uuid
%param-PoUuid = ls_target-po_uuid
%is_draft = if_abap_behv=>mk-on )
TO lt_drafts.
ENDLOOP.
" 2) Draft 인스턴스에 대해 Activate 액션 호출
MODIFY ENTITIES OF zr_purchaseorder001
ENTITY PurchaseOrder
EXECUTE Activate
FROM lt_drafts
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
" 3) 결과 로깅
IF ls_failed-purchaseorder IS NOT INITIAL.
LOOP AT ls_failed-purchaseorder INTO DATA(ls_fail).
MESSAGE |Activate failed for { ls_fail-PoUuid }|
TYPE 'E' INTO DATA(lv_msg).
" 어플리케이션 로그(SLG1) 적재 권장
ENDLOOP.
RETURN.
ENDIF.
" 4) 커밋 — RAP 트랜잭션 매니저를 통해 저장
COMMIT ENTITIES RESPONSES
FAILED DATA(ls_commit_failed)
REPORTED DATA(ls_commit_reported).
ENDMETHOD.
여기서 핵심 포인트는 두 가지입니다. 첫째, %is_draft = if_abap_behv=>mk-on을 명시해 "지금 호출하는 인스턴스는 Draft 측이다"라고 프레임워크에 알려야 합니다. 이를 빠뜨리면 Active 테이블에서 키를 찾으려 시도해 not-found 오류가 발생합니다. 둘째, MODIFY ENTITIES 만으로는 DB에 반영되지 않으며 반드시 COMMIT ENTITIES를 호출해야 트랜잭션 버퍼가 영속화됩니다.
3단계 — Validation 훅과 production 수준 안전장치
2단계까지 작성한 코드는 동작하지만 검증 누락 위험이 있습니다. Validation 클래스를 BIL에 구현하고 Draft 단계에서도 사전 검증이 일어나도록 보완합니다.
CLASS lhc_purchaseorder IMPLEMENTATION.
METHOD validateSupplier.
READ ENTITIES OF zr_purchaseorder001 IN LOCAL MODE
ENTITY PurchaseOrder
FIELDS ( PoUuid Supplier )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_pos).
" 활성화된 거래처만 허용
SELECT supplier_id FROM zsupplier_a
FOR ALL ENTRIES IN @lt_pos
WHERE supplier_id = @lt_pos-Supplier
AND active_flag = @abap_true
INTO TABLE @DATA(lt_valid).
LOOP AT lt_pos INTO DATA(ls_po).
IF NOT line_exists( lt_valid[ supplier_id = ls_po-Supplier ] ).
APPEND VALUE #( %tky = ls_po-%tky ) TO failed-purchaseorder.
APPEND VALUE #( %tky = ls_po-%tky
%state_area = 'VALIDATE_SUPPLIER'
%msg = new_message(
id = 'ZPO_MSG'
number = '001'
severity = if_abap_behv_message=>severity-error
v1 = ls_po-Supplier ) )
TO reported-purchaseorder.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
실제 운영 환경에서는 다음 항목을 추가로 고려해야 합니다.
- 인증/권한: BDEF의
authorization master ( instance )와FOR INSTANCE AUTHORIZATION메서드를 함께 구현해 Draft 활성화 권한을 별도로 통제합니다. - 락 충돌: 동일 Active 인스턴스에 대해 두 사용자가 동시에 Edit Draft를 만들 수 있는지 정책을 결정해야 합니다.
shared draft를 선언하면 협업이 가능하지만 충돌 처리 로직이 추가로 필요합니다. - 성능:
draft action Activate optimized사용, Draft 테이블에 적절한 인덱스,READ ENTITIES IN LOCAL MODE로 권한 체크 우회(내부 호출만) 등을 활용합니다. - 테스트: ABAP Unit +
CL_CDS_TEST_ENVIRONMENT/CL_BUS_ABSTRACT_BO_TEST_ENV로 Active/Draft 테이블을 모킹해 회귀 테스트를 작성합니다.
자주 발생하는 실수 3가지와 해결책
Q1. Fiori Elements에서 Edit 버튼이 보이지 않습니다.
가장 흔한 원인은 BDEF에 with draft 선언이 누락된 경우입니다. draft table, draft action Edit/Activate/Discard/Resume, draft determine action Prepare 네 가지 중 하나라도 빠지면 OData 메타데이터에서 Draft 관련 함수가 노출되지 않아 UI가 Edit 버튼을 렌더링하지 않습니다. Service Binding을 다시 활성화하고, ADT의 "Active Annotations" 미리보기로 DraftRoot가 정상 생성됐는지 확인하세요.
Q2. EML로 Activate를 호출했는데 "Draft entity not found" 또는 활성화 자체는 성공했지만 Active 테이블에 값이 안 들어옵니다.
두 가지 원인이 결합돼 있는 경우가 많습니다. 첫째, %is_draft = if_abap_behv=>mk-on을 누락해 Active 측에서 키를 검색합니다. 둘째, COMMIT ENTITIES를 호출하지 않아 트랜잭션 버퍼에만 머무릅니다. EML은 메모리상 버퍼를 조작하고, 실제 DB 반영은 명시적 커밋 시점에 일어납니다. 또한 FAILED/REPORTED 응답을 반드시 검사해 무음 실패를 막아야 합니다.
Q3. validation이 호출되긴 하는데 Draft 단계에서는 메시지가 안 보입니다.
validation ... on save로만 선언하면 ACTIVATE 시점에만 호출됩니다. Draft 입력 중 실시간 검증을 원한다면 draft determine action Prepare 블록에 동일 validation을 등록해야 합니다. 또한 reported 테이블에 %state_area를 지정해야 같은 검증 키로 재실행 시 이전 메시지가 자동 정리됩니다. 이걸 빠뜨리면 사용자가 값을 고쳐도 옛 오류 메시지가 그대로 남아 보입니다.
이어서 살펴볼 만한 주제
Draft Activate를 한 번 끝내면 자연스럽게 다음 주제로 확장됩니다.
- Shared Draft: 여러 사용자가 같은 Draft를 공동 편집하는 시나리오,
with shared draft옵션과 락 정책 - Unmanaged Save: Active 테이블이 레거시 BAPI/Function Module 뒤에 숨겨진 경우,
saver클래스 직접 구현 - Side Effects 어노테이션: Draft 필드 변경 시 다른 필드 자동 재계산을 클라이언트가 인지하도록 OData 어노테이션 추가
- Background processing:
commit entities in background와 비동기 활성화 패턴 - ABAP Unit + Test Doubles:
cl_abap_behv_test_environment를 활용해 Draft 시나리오를 격리 테스트
더 깊이 학습할 수 있는 자료
- SAP Help Portal — Draft in ABAP RAP 개요 문서
- SAP Help Portal — CDS Behavior Definitions(BDEF) 문법 레퍼런스
- SAP Help Portal — Entity Manipulation Language(EML) 가이드
- SAP Developers — Develop a Draft-Enabled RAP Business Object 미션
- SAP Community Blogs — RAP Draft 태그 모음
- GitHub SAP-samples/abap-platform-rap100 — RAP 기본 학습 리포지토리
댓글 0
아직 댓글이 없습니다.
💬 댓글 작성은 UI5 모드에서 사용할 수 있습니다.
UI5 모드에서 사용하기