개요 및 이 글에서 다룰 것
RAP(RESTful ABAP Programming Model)에서 키 값을 언제 결정할지는 비즈니스 요구사항을 좌우하는 중요한 설계 결정입니다. Early Numbering이 인스턴스 생성 직후 키를 부여하는 방식이라면, Late Numbering은 SAVE 단계, 즉 트랜잭션이 DB에 반영되기 직전에 키를 할당합니다. 이 글에서는 왜 저장 시점 키 할당이 필요한지, 어떻게 구현하는지, 그리고 동시성/성능/감사(audit) 관점에서 무엇을 주의해야 하는지를 다룹니다.
- Late Numbering의 동작 시점과 RAP 트랜잭션 단계 이해
- Managed BO와 Unmanaged BO에서의 Late Numbering 구현 차이
- Number Range Object(NRO)와의 연동 패턴
- %pid(Preliminary ID)와 매핑 테이블 처리
- 흔한 실수와 트러블슈팅 시나리오
먼저 알고 있으면 좋은 것
이 글은 advanced 난이도로, RAP의 기본 BDEF 문법(define behavior for, create, update, delete), Managed/Unmanaged 시나리오 구분, EML(Entity Manipulation Language)의 MODIFY/COMMIT ENTITIES 흐름을 이미 알고 있다고 가정합니다. 또한 SNRO 트랜잭션의 Number Range Object 생성 경험과 CL_NUMBERRANGE_RUNTIME API에 대한 이해가 있으면 좋습니다.
환경, 버전, 준비물
실습 환경은 일반적으로 다음을 권장합니다.
- SAP S/4HANA Cloud Public Edition 2402 이상 또는 ABAP Platform 2022(7.58) 이상
- ABAP Development Tools(ADT) for Eclipse 최신 빌드
- SAP BTP ABAP Environment(Steampunk) 사용 시 동일하게 적용
- Number Range Object: SNRO 트랜잭션에서 사전 정의(예: ZSO_NUM)
- 테이블, CDS View, Behavior Definition, Behavior Implementation 클래스 권한
Late Numbering은 Managed/Unmanaged 모두에서 지원되지만, 본 예제는 Managed with additional save를 기준으로 합니다. Unmanaged의 경우 saver 클래스에서 동일한 콜백을 구현하는 형태입니다.
핵심 개념: Late Numbering이 풀어주는 문제
RAP 트랜잭션은 크게 Interaction Phase와 Save Sequence 두 단계로 나뉩니다. Interaction Phase에서 사용자는 여러 번 CREATE/UPDATE/DELETE를 호출할 수 있고, 이때 인스턴스는 트랜잭션 버퍼에만 존재합니다. Save Sequence는 finalize → check_before_save → adjust_numbers → save → cleanup 순서로 진행되며, Late Numbering은 정확히 adjust_numbers 단계에서 실행됩니다.
비유하자면, Early Numbering은 식당 입구에서 대기표를 받는 방식이고, Late Numbering은 실제로 자리에 앉은 손님에게만 영수증 번호를 부여하는 방식입니다. 중간에 자리를 비우거나 취소하는 손님에게는 번호가 낭비되지 않습니다.
왜 저장 시점에 번호를 부여해야 할까요? 다음 시나리오를 고려해봅시다.
- 번호 갭(gap) 최소화: 사용자가 Draft에서 여러 번 생성/취소를 반복하더라도 실제로 저장되는 인스턴스에만 번호가 소비됩니다. 세무·감사 요건상 연속 번호가 중요한 송장(Invoice), 회계 전표 등에 적합합니다.
- 동시성 충돌 회피: 인스턴스 생성마다 NRO를 잠그면 락 경합이 커집니다. 저장 직전에 일괄 할당하면 락 보유 시간이 짧아집니다.
- 업무 규칙 기반 번호 체계: 회사 코드, 회계 연도, 문서 유형에 따라 번호 범위가 달라지는 경우, 모든 필드가 확정된 SAVE 시점에 번호를 정해야 일관성이 보장됩니다.
Late Numbering의 핵심 기제는 %pid(Preliminary ID)입니다. Interaction Phase 동안 클라이언트는 실제 키 대신 임시 ID로 인스턴스를 참조하고, adjust_numbers에서 실제 키로 매핑됩니다. 이 매핑 테이블을 framework가 자동 전파하므로, 후속 association이나 child entity의 외래 키도 올바르게 재배선됩니다.
실전 코드 1단계: 기본 Late Numbering 활성화
판매 주문(SalesOrder) 엔티티에서 OrderID를 Late Numbering으로 부여하는 예제입니다. 먼저 Behavior Definition을 봅시다.
managed implementation in class zbp_r_salesorder_tx unique;
strict ( 2 );
with draft;
define behavior for ZR_SalesOrderTX alias SalesOrder
persistent table zsales_order_t
draft table zsales_order_d
lock master
total etag ChangedAt
authorization master ( instance )
{
field ( numbering : managed, readonly ) OrderID;
field ( readonly ) CreatedAt, CreatedBy, ChangedAt, ChangedBy;
create;
update;
delete;
draft action Activate optimized;
draft action Discard;
late numbering;
mapping for zsales_order_t
{
OrderID = order_id;
CustomerID = customer_id;
TotalAmount = total_amount;
ChangedAt = changed_at;
}
}
핵심은 두 가지입니다. field ( numbering : managed )로 키 필드가 framework에 의해 채워진다고 선언하고, late numbering;으로 시점을 명시합니다. 이렇게 하면 Behavior Pool 클래스에 adjust_numbers 메서드 시그니처가 자동 노출됩니다.
실전 코드 2단계: adjust_numbers 구현 + Number Range 연동
실제 번호 할당 로직과 매핑 테이블 채우기, 그리고 NRO 호출 실패에 대한 처리 패턴입니다.
CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS adjust_numbers REDEFINITION.
ENDCLASS.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD adjust_numbers.
IF mapped-salesorder IS INITIAL.
RETURN.
ENDIF.
DATA(lv_quantity) = lines( mapped-salesorder ).
TRY.
cl_numberrange_runtime=>number_get(
EXPORTING
nr_range_nr = 01
object = ZSO_NUM
quantity = lv_quantity
IMPORTING
number = DATA(lv_next_num)
returncode = DATA(lv_returncode)
returned_quantity = DATA(lv_returned_qty) ).
IF lv_returned_qty < lv_quantity.
APPEND VALUE #(
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Number range exhausted for ZSO_NUM| )
) TO reported-salesorder.
RETURN.
ENDIF.
CATCH cx_number_ranges INTO DATA(lx_nr).
APPEND VALUE #(
%msg = new_message(
id = ZSO_MSG
number = 001
severity = if_abap_behv_message=>severity-error
v1 = lx_nr->get_text( ) )
) TO reported-salesorder.
RETURN.
ENDTRY.
DATA(lv_current_num) = CONV i( lv_next_num ) - lv_quantity + 1.
LOOP AT mapped-salesorder ASSIGNING FIELD-SYMBOL().
-OrderID =
|SO{ lv_current_num WIDTH = 10 ALIGN = RIGHT PAD = '0' }|.
lv_current_num = lv_current_num + 1.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
여기서 주목할 점은 mapped-salesorder 테이블이 입력으로 들어올 때 이미 %pid와 %tmp 키가 채워져 있다는 것입니다. 우리가 채워야 하는 건 실제 영속 키(OrderID)입니다. NRO 호출은 한 번에 quantity 단위로 받아오는 것이 락 경합을 줄이는 핵심입니다.
실전 코드 3단계: 자식 엔티티 + 부분 실패 처리
SalesOrder가 SalesOrderItem이라는 자식 엔티티를 가질 때, 부모 키 할당 후 자식의 외래 키도 framework가 자동으로 매핑해야 합니다.
METHOD adjust_numbers.
" === 부모 SalesOrder 번호 할당 ===
IF mapped-salesorder IS NOT INITIAL.
DATA(lv_qty_parent) = lines( mapped-salesorder ).
cl_numberrange_runtime=>number_get(
EXPORTING
nr_range_nr = 01
object = ZSO_NUM
quantity = lv_qty_parent
IMPORTING
number = DATA(lv_last_parent)
returncode = DATA(lv_rc_parent) ).
IF lv_rc_parent = 3.
cl_bali_log=>get_instance( )->add_free_text(
severity = if_bali_constants=>c_severity_warning
text = |ZSO_NUM range above critical threshold| ).
ENDIF.
DATA(lv_run_parent) = CONV i( lv_last_parent ) - lv_qty_parent + 1.
LOOP AT mapped-salesorder ASSIGNING FIELD-SYMBOL().
-OrderID =
|SO{ lv_run_parent WIDTH = 10 ALIGN = RIGHT PAD = '0' }|.
lv_run_parent = lv_run_parent + 1.
ENDLOOP.
ENDIF.
" === 자식 SalesOrderItem 번호 할당 ===
IF mapped-salesorderitem IS NOT INITIAL.
DATA(lv_qty_item) = lines( mapped-salesorderitem ).
cl_numberrange_runtime=>number_get(
EXPORTING
nr_range_nr = 01
object = ZSOITM_NUM
quantity = lv_qty_item
IMPORTING
number = DATA(lv_last_item) ).
DATA(lv_run_item) = CONV i( lv_last_item ) - lv_qty_item + 1.
LOOP AT mapped-salesorderitem ASSIGNING FIELD-SYMBOL().
-ItemPosition =
|{ lv_run_item WIDTH = 6 ALIGN = RIGHT PAD = '0' }|.
lv_run_item = lv_run_item + 1.
ENDLOOP.
ENDIF.
ENDMETHOD.
운영 환경에서는 다음을 추가로 권장합니다. NRO buffering 설정: SNRO에서 메인 메모리 버퍼링을 활성화하면 번호 발급 성능이 향상되지만, 갭이 발생할 수 있어 연속성이 필수인 케이스에서는 No buffering을 선택합니다. 단위 테스트: ABAP Unit에서 NRO mock을 주입합니다. 권한: NRO 자체에 S_NUMBER 권한 객체가 적용되므로, 비즈니스 사용자 역할에 적절히 부여해야 합니다.
흔한 실수와 트러블슈팅
Q1. adjust_numbers에서 키를 채웠는데 DB에 NULL/공백으로 저장됩니다. 가장 흔한 원인은 mapped-entity의 행을 LOOP INTO로 복사 변수에 담은 후 MODIFY를 빠뜨린 경우입니다. ASSIGNING FIELD-SYMBOL로 직접 참조하거나, 변경 후 명시적으로 MODIFY mapped-... FROM ls_row를 호출해야 합니다.
Q2. early numbering으로 작성된 BDEF에 late numbering 키워드만 추가했는데 동작이 이상합니다. Early와 Late는 호출 시점뿐 아니라 framework가 %cid/%pid를 처리하는 방식이 다릅니다. 외부 호출 코드가 응답으로 받은 %pid → key 매핑을 다시 읽도록 수정해야 합니다.
Q3. 동시 사용자 트래픽에서 같은 번호가 두 번 발급됩니다. SELECT MAX(id) + 1 같은 패턴을 사용했을 가능성이 큽니다. 반드시 cl_numberrange_runtime=>number_get 또는 SNRO 기반 API를 사용해야 직렬화가 보장됩니다.
Q4. Draft Activate 시 reported 메시지가 사라집니다. adjust_numbers는 Save Sequence 단계라 일부 reported 처리가 클라이언트에 전파되지 않을 수 있습니다. 운영상 중요한 검증은 check_before_save에서 수행하고, adjust_numbers는 단순 번호 할당에 집중하는 것이 권장됩니다.
이후 살펴볼 만한 주제
Late Numbering을 익혔다면 다음 주제로 확장하는 것을 권장합니다. Unmanaged Late Numbering: saver 클래스의 adjust_numbers를 직접 구현하는 패턴으로, 레거시 함수 모듈과 통합할 때 유용합니다. Semantic Key vs Technical Key: UUID를 기술 키로, 비즈니스 번호를 시맨틱 키로 이중화하여 Early+Late 혼합 전략을 적용하는 방법. Number Range Interval Maintenance: SAP BTP ABAP Environment의 SNRO 대체 앱과 운영 모니터링. Side Effects와 Determinations: 번호 할당 후 자동으로 파생 필드를 계산하는 패턴.
댓글 0
아직 댓글이 없습니다.