개요 및 학습 흐름
SAP RAP(RESTful ABAP Programming Model)에서 비즈니스 키(primary key)를 어느 시점에 확정할 것인가는 트랜잭션 설계의 근본 결정 사항입니다. RAP는 이를 위해 Early Numbering과 Late Numbering이라는 두 가지 메커니즘을 제공하며, 각 방식은 number range 호출 시점, 초안(draft) 처리, key mapping 필요 여부에서 본질적으로 다릅니다. 이 글은 두 방식의 트랜잭션 흐름을 단계별로 해부하고 실전 코드까지 다룹니다.
- Early/Late Numbering의 동작 시점과 차이를 트랜잭션 단계별로 식별
- BDEF에서 numbering managed/unmanaged 선언 패턴 작성
- adjust_numbers, save_modified 핸들러 메서드 구현 흐름 파악
- preliminary key와 final key 간 key mapping 처리 로직 작성
- 실무 시나리오(영업 주문, 구매 요청)에서 적합한 방식 선택
읽기 전 알아두면 좋은 배경
이 글은 RAP의 Behavior Definition(BDEF), Behavior Implementation(클래스 풀), draft 처리 기본 개념을 어느 정도 다뤄본 개발자를 대상으로 합니다. CDS view, projection view, managed/unmanaged scenario, ABAP OO에서 인터페이스 구현 방식, NUMBER RANGE 오브젝트(SNRO)의 기본 동작을 알고 있다면 큰 무리 없이 따라갈 수 있습니다. EML(Entity Manipulation Language)의 MODIFY ENTITIES 구문에 익숙하면 코드 해석이 한층 수월합니다.
실행 환경 및 사전 준비
이 글에서 다루는 코드는 일반적으로 다음 환경을 전제로 합니다. SAP S/4HANA Cloud Private Edition 2022 이상 또는 SAP BTP ABAP Environment(Steampunk) 최신 릴리스에서 동작이 검증되며, ABAP Development Tools(ADT) for Eclipse 2023-12 이상을 권장합니다.
- 릴리스: ABAP Platform 2022 / S/4HANA 2023 / BTP ABAP Environment
- 도구: ABAP Development Tools(Eclipse) 최신
- SNRO 트랜잭션을 통한 Number Range Object 생성 권한
- RAP managed scenario 또는 unmanaged scenario 경험
- 샘플 도메인: 영업 주문(ZSO_ROOT) 루트 엔티티 가정
이전에 BO(Business Object)를 만들어 본 적이 있다면 동일 패키지에 새 root entity를 추가해 실습하는 것을 권장합니다. 일반적으로 운영 시스템이 아닌 개발 클라이언트에서 진행해야 안전합니다.
두 가지 키 할당 방식의 본질
RAP에서 numbering이란 "비즈니스 키 값을 누가, 언제, 어떻게 채워주는가"에 대한 약속입니다. 이를 음식점에 비유해 보겠습니다. Early Numbering은 손님이 입장하자마자 테이블 번호(확정 키)를 즉시 부여받는 방식입니다. 주문이 끝나든 안 끝나든 그 번호는 손님 것입니다. 반면 Late Numbering은 임시 대기표(preliminary key, 보통 GUID)를 받고 자리에 앉아 주문을 마친 뒤 결제(SAVE) 시점에 비로소 영수증 번호(final key)가 발급되는 방식입니다.
RAP는 numbering을 두 축으로 분류합니다. 첫째는 numbering을 "누가" 하느냐(managed = 프레임워크, unmanaged = 개발자). 둘째는 "언제" 하느냐(early = MODIFY 시점, late = SAVE 시점). 조합하면 다음과 같습니다.
- Managed Early Numbering: 프레임워크가 CREATE 직후 키를 채워준다(주로 UUID 자동).
- Unmanaged Early Numbering: 개발자가 adjust_numbers 메서드에서 number range를 호출해 즉시 키를 부여한다.
- Late Numbering: 항상 unmanaged. 초안 단계에서는 preliminary key가 사용되고, SAVE 단계의 save_modified 같은 메서드에서 final key가 매핑된다.
Late Numbering의 핵심은 key mapping입니다. 초안에서 사용하던 preliminary key를 최종 키로 바꾸면서, RAP는 %pre / %key 매핑 테이블을 통해 자식 엔티티의 외래키까지 일관되게 갱신해야 합니다. 즉 Late Numbering은 "키가 바뀐다"는 사실 때문에 자식 엔티티 참조, 인증 시스템, OData 응답 ETag까지 모두 영향을 받을 수 있어 설계 부담이 큽니다.
반면 Early Numbering은 키가 트랜잭션 초기에 확정되므로 외부 시스템 연동, 자식 엔티티 생성, 사용자 화면 표시가 모두 단순해집니다. 그러나 사용자가 화면을 닫고 저장하지 않을 경우 number range가 "구멍"을 남길 수 있다는 단점이 있습니다. 회계, 송장처럼 갭(gap)을 허용하지 않는 도메인에서는 Late Numbering이 일반적으로 선호됩니다.
실전 예제: 영업 주문 BO에 적용하기
1단계 — Managed Early Numbering 기본 형태
가장 단순한 시나리오는 키를 UUID로 잡고 프레임워크에 위임하는 방식입니다. BDEF는 다음처럼 작성합니다.
managed implementation in class zbp_so_root unique;
strict ( 2 );
with draft;
define behavior for ZI_SalesOrderRoot alias SalesOrder
persistent table zsod_so_root
draft table zsod_so_draft
lock master
authorization master ( instance )
etag master LocalLastChangedAt
{
field ( numbering : managed, readonly ) SalesOrderUUID;
field ( readonly ) SalesOrderId, CreatedAt, CreatedBy;
field ( mandatory ) CustomerId, OrderDate;
create;
update;
delete;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft determine action Prepare;
}
이 선언에서 numbering : managed는 SalesOrderUUID 필드를 RAP 런타임이 CREATE 직후 자동으로 채우게 합니다. 비즈니스 키인 SalesOrderId는 별도 처리가 필요합니다(다음 단계).
2단계 — Unmanaged Early Numbering으로 SalesOrderId 채우기
실무에서는 UUID 외에 사람이 읽을 수 있는 비즈니스 키(예: "SO-2026-000123")가 필요합니다. SNRO에서 ZSO_RANGE 오브젝트를 만들었다고 가정하고 Early Numbering 핸들러를 추가합니다. BDEF에 다음을 더합니다.
field ( numbering : managed ) SalesOrderUUID;
field ( readonly ) SalesOrderId;
create ( precheck );
mapping for zsod_so_root
{
SalesOrderUUID = uuid;
SalesOrderId = so_id;
CustomerId = customer_id;
OrderDate = order_date;
}
그리고 동작 구현 클래스에서 ADJUST_NUMBERS 핸들러를 작성합니다. 일반적으로 EARLY NUMBERING은 FOR NUMBERING 메서드 그룹에 포함됩니다.
METHODS adjust_numbers FOR NUMBERING
IMPORTING entities FOR CREATE SalesOrder.
METHOD adjust_numbers.
DATA(lv_year) = |{ sy-datum(4) }|.
LOOP AT entities INTO DATA(ls_entity).
CALL FUNCTION 'NUMBER_GET_NEXT'
EXPORTING
nr_range_nr = '01'
object = 'ZSO_RANGE'
IMPORTING
number = DATA(lv_next)
EXCEPTIONS
OTHERS = 8.
IF sy-subrc <> 0.
APPEND VALUE #( %cid = ls_entity-%cid
%key = ls_entity-%key
%msg = new_message( id = 'ZSO_MSG'
number = '001'
severity = if_abap_behv_message=>severity-error ) )
TO failed-salesorder.
CONTINUE.
ENDIF.
APPEND VALUE #( %cid = ls_entity-%cid
%key = ls_entity-%key
SalesOrderId = |SO-{ lv_year }-{ lv_next ALIGN = RIGHT PAD = '0' WIDTH = 6 }| )
TO mapped-salesorder.
ENDLOOP.
ENDMETHOD.
중요한 점은 이 메서드가 CREATE 트랜잭션 단계 내부에서 호출된다는 사실입니다. 즉 사용자가 신규 주문을 화면에서 작성하기 시작한 순간 이미 SalesOrderId가 결정되며, draft 테이블에도 그 값이 들어갑니다. 사용자가 저장 없이 화면을 닫으면 그 번호는 일반적으로 사용되지 않은 채 남습니다 — gap 발생 가능성.
3단계 — Late Numbering으로 갭 없는 키 발급 + 로깅
회계성 도메인(예: ZI_PurchaseInvoice)에서는 Late Numbering이 권장됩니다. BDEF는 unmanaged numbering을 명시합니다.
managed implementation in class zbp_pinv_root unique;
strict ( 2 );
with draft;
define behavior for ZI_PurchaseInvoice alias Invoice
late numbering
persistent table zinv_root
draft table zinv_draft
lock master
authorization master ( instance )
{
field ( numbering : managed, readonly ) InvoiceUUID;
field ( readonly ) InvoiceNumber, PostingDate;
field ( mandatory ) SupplierId, GrossAmount;
create;
update;
delete;
draft action Edit;
draft action Activate optimized;
}
핵심은 late numbering 키워드입니다. 이렇게 선언하면 RAP는 CREATE 시점에 비즈니스 키 InvoiceNumber를 채우지 않으며, SAVE 단계에서 호출되는 adjust_numbers 메서드(late variant)에서 값을 부여해야 합니다.
METHODS adjust_numbers REDEFINITION.
METHOD adjust_numbers.
DATA: lt_log TYPE STANDARD TABLE OF zinv_log.
LOOP AT mapped-invoice ASSIGNING FIELD-SYMBOL().
TRY.
cl_numberrange_runtime=>number_get(
EXPORTING nr_range_nr = '02'
object = 'ZINV_RANGE'
quantity = '1'
IMPORTING number = DATA(lv_no)
returncode = DATA(lv_rc) ).
-InvoiceNumber = |INV-{ sy-datum(4) }-{ lv_no ALIGN = RIGHT PAD = '0' WIDTH = 8 }|.
APPEND VALUE #( log_uuid = cl_system_uuid=>create_uuid_x16_static( )
invoice_uuid = -InvoiceUUID
invoice_no = -InvoiceNumber
created_at = cl_abap_context_info=>get_system_date( ) ) TO lt_log.
CATCH cx_number_ranges INTO DATA(lx_nr).
APPEND VALUE #( %key = -%key
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_nr->get_text( ) ) )
TO reported-invoice.
ENDTRY.
ENDLOOP.
IF lt_log IS NOT INITIAL.
INSERT zinv_log FROM TABLE @lt_log.
ENDIF.
ENDMETHOD.
여기서 주의할 점은 mapped-invoice에 들어있는 InvoiceUUID는 final UUID로 이미 확정되어 있다는 것입니다. preliminary 단계에서 자식 엔티티가 외래키로 사용하던 UUID가 SAVE에 들어오기 전 final로 바뀌었다면, RAP 런타임이 자동으로 자식 엔티티의 부모 참조를 갱신합니다. 개발자는 비즈니스 번호(InvoiceNumber)만 책임지면 됩니다. 트랜잭션 흐름은 일반적으로 다음 순서로 진행됩니다.
- CREATE: preliminary UUID + InvoiceNumber 공백 상태로 draft 저장
- UPDATE: 사용자가 입력 항목 보완
- ACTIVATE: draft를 활성 인스턴스로 전환 요청
- FINALIZE → CHECK_BEFORE_SAVE → ADJUST_NUMBERS(late) → SAVE
- 최종 키가 부여된 인스턴스가 영구 테이블에 기록
운영 환경에서는 추가로 다음 보강 사항을 권장합니다. 첫째, NUMBER_GET_NEXT 대신 클래스 기반 cl_numberrange_runtime을 사용해 예외 처리를 일관화합니다. 둘째, SAVE 단계의 모든 키 할당은 자체 로그 테이블에 흔적을 남겨 감사 추적이 가능해야 합니다. 셋째, 부하 테스트 시 number range buffering 옵션(SNRO에서 No Buffering / Main Memory Buffering)을 도메인 특성에 맞게 결정합니다 — 갭을 절대 허용하지 않아야 한다면 No Buffering이 일반적으로 권장됩니다.
자주 부딪히는 오류와 해결 단서
Q1. Late Numbering인데 ADJUST_NUMBERS가 호출되지 않습니다. BDEF에 late numbering 키워드를 명시했는지, 그리고 behavior implementation 클래스에 해당 메서드가 REDEFINITION으로 선언되어 있는지 확인하십시오. 또한 BDEF의 root 엔티티에만 late numbering이 선언되어 있어야 하며, 자식 엔티티에 잘못 선언했다면 활성화 시 경고가 발생할 수 있습니다.
Q2. Early Numbering에서 같은 번호가 두 번 발급됩니다. 일반적으로 SNRO 오브젝트의 buffer 설정 문제이거나, adjust_numbers 내부에서 LOOP를 돌면서 NUMBER_GET_NEXT를 호출할 때 quantity를 1로만 주고 있을 가능성이 큽니다. 대량 CREATE 상황에서는 quantity = lines( entities )로 한 번에 발급받아 분배하는 패턴이 권장됩니다.
Q3. Late Numbering에서 OData 응답의 키가 draft 키와 다릅니다. 이는 의도된 동작입니다. SAVE 직후 RAP는 새 키 매핑을 클라이언트에 응답으로 돌려주며, Fiori Elements 런타임은 이를 받아 자동으로 navigation을 갱신합니다. 커스텀 UI라면 응답의 %pid → %key 매핑을 수동으로 처리해야 합니다.
추가로 자주 발생하는 함정으로는 (1) Early Numbering에서 number range 권한 부족으로 CREATE 자체가 실패, (2) Late Numbering에서 SAVE 단계 예외가 발생하면 트랜잭션 전체가 롤백되어 사용자가 입력한 모든 내용이 사라짐, (3) preliminary key를 외부 시스템에 미리 알려준 뒤 final key로 바뀌면서 외부 데이터 정합성 깨짐 등이 있습니다. 외부 연동이 잦은 도메인이라면 Early Numbering이 일반적으로 더 안전합니다.
이어서 살펴볼 주제
이 글에서 다룬 numbering 메커니즘은 RAP BO 설계의 출발점일 뿐입니다. 이어서 다음 주제를 살펴볼 것을 권장합니다.
- RAP Managed Scenario의 transactional buffer 동작과 numbering 상호작용
- Draft-enabled BO에서의 ETag 관리와 동시성 제어
- Unmanaged Save 시 SAVE_MODIFIED 메서드에서의 일관성 보장 패턴
- BTP ABAP Environment에서 number range를 ABAP Cloud 호환 API로 다루는 방법
- 실제 영업/구매 도메인에서 Early vs Late 의사결정 매트릭스 작성
더 깊게 읽을 자료
- SAP Help Portal — ABAP RAP: Early Numbering
- SAP Help Portal — ABAP RAP: Late Numbering
- SAP Help Portal — Numbering (Overview)
- SAP Help Portal — CDS Behavior Definitions(BDEF)
- SAP Developers — Develop a Managed RAP Business Object
- SAP Community — RAP Numbering 관련 블로그 모음
- SAP Help Portal — EML MODIFY ENTITIES 구문
댓글 0
아직 댓글이 없습니다.