이 글에서 다루는 내용과 도달 지점
SAP RAP(RESTful Application Programming Model)에서 %cid는 단일 트랜잭션 내에서 아직 데이터베이스에 커밋되지 않은 신규 인스턴스를 식별하기 위한 임시 키(temporary key)입니다. 특히 부모-자식 관계를 한 번의 EML(Entity Manipulation Language) 호출로 생성하는 Deep Create 시나리오에서는 %cid 없이 자식 엔터티의 부모 참조를 표현할 수 없습니다. 이 글에서는 %cid의 의미부터 Deep Create 동작 원리, 누락 시 발생하는 실제 오류, 그리고 운영급 코드까지 단계별로 정리합니다.
%cid/%cid_ref의 역할과 라이프사이클 이해- SalesOrder + SalesOrderItem 시나리오로 Deep Create 구현
%cid누락 시 발생하는CX_SY_REF_IS_INITIAL·키 충돌 오류 분석- 매니지드/언매니지드 시나리오에서의 차이점 및 로깅 패턴 정리
이 글을 읽기 전에 알아두면 좋은 것
ABAP RAP의 BDEF(Behavior Definition) 구문, EML의 MODIFY ENTITIES 키워드, CDS 뷰 엔터티(View Entity)와 컴포지션(composition of) 개념을 미리 익혀두면 이 글이 훨씬 수월합니다. 매니지드 시나리오 기반의 CRUD 동작 흐름과 키 처리 방식(early/late numbering)에 대한 사전 이해도 권장됩니다.
실행 환경과 도구 준비
이 글의 예제는 다음 환경에서 검증하는 것을 기준으로 작성되었습니다.
- SAP S/4HANA Cloud Public Edition 2402 이상 또는 ABAP Platform 2023 on-premise
- ABAP Cloud / ABAP RESTful Application Programming Model (managed scenario, draft 미사용)
- ADT(ABAP Development Tools) for Eclipse 2024 이상
- 샘플 데이터 패키지:
ZRAP_SALES_ORDER(네임스페이스는 사내 규칙에 맞게 변경) - SAP Gateway Client / Postman / Bruno 등 OData v4 호출 도구
BDEF에서는 부모 엔터티 SalesOrder를 루트로, 자식 SalesOrderItem을 컴포지션으로 정의했다고 가정합니다. 키 필드는 부모가 SalesOrderId(CHAR10), 자식은 SalesOrderId + ItemPosition(NUMC4)입니다. 키 생성 방식은 RAP의 early numbering을 사용하지만, late numbering에서도 %cid 자체의 의미는 동일합니다.
%cid의 의미와 Deep Create 내부 동작
%cid는 "Content ID"의 약자로, OData v4의 batch / change set 내부에서 사용되는 Content-ID 헤더 개념을 RAP가 EML 레벨로 끌어올린 것입니다. 한 번의 MODIFY ENTITIES 호출 안에서 새로 만들어지는 인스턴스는 아직 영구 키(persistent key)를 가지지 않습니다. 이때 클라이언트/호출 측이 임의로 부여하는 문자열이 바로 %cid입니다.
비유하자면, 회의실 예약 시스템에서 "회의실 A"가 확정되기 전까지는 "임시 라벨 #1"이라고 부르는 것과 같습니다. 회의가 끝나고 시스템이 라벨 #1을 "회의실 A"로 확정하는 순간, 그 임시 라벨은 더 이상 의미가 없습니다. RAP도 동일하게, 트랜잭션 종료 시점에 %cid는 폐기되고 진짜 키 값이 부여됩니다.
핵심:
%cid는 "이 호출 동안만 유효한" 가짜 키이며, 자식 인스턴스를 만들 때 부모를 가리키는 포인터(%cid_ref)의 종착점이 됩니다.
Deep Create는 한 번의 EML 호출로 루트와 그 하위 컴포지션 자식 인스턴스를 동시에 생성하는 패턴입니다. 자식 입장에서는 "어떤 부모 밑에 들어갈지"를 표현해야 하는데, 부모도 같은 호출에서 새로 만들어지므로 DB 키가 아직 존재하지 않습니다. 따라서 자식의 _Parent 연관(association)을 %cid_ref로 표현해 "이 트랜잭션 안에서 %cid가 X인 부모를 가리킨다"고 선언합니다.
RAP 런타임은 내부적으로 %cid → 임시 인스턴스 핸들 매핑 테이블을 유지하며, FINALIZE → CHECK_BEFORE_SAVE → SAVE 단계에서 이 매핑을 영구 키로 교체합니다. 매핑이 끊기는 가장 흔한 원인이 바로 자식에서 %cid_ref를 사용하지 않고 부모의 임시 키 필드를 직접 채우거나, 부모에서 %cid를 부여하지 않는 것입니다.
실전 예제 1단계: 기본형 Deep Create
가장 단순한 형태의 Deep Create부터 살펴봅니다. 부모 SalesOrder 한 건과 자식 SalesOrderItem 두 건을 한 번에 생성합니다.
DATA orders_to_create TYPE TABLE FOR CREATE zi_sales_order.
DATA items_to_create TYPE TABLE FOR CREATE zi_sales_order\_Item.
orders_to_create = VALUE #(
( %cid = 'SO_001'
customer_id = '10000042'
order_date = cl_abap_context_info=>get_system_date( )
currency_code = 'EUR' )
).
items_to_create = VALUE #(
( %cid_ref = 'SO_001'
%target = VALUE #(
( %cid = 'ITM_001'
item_position = '0010'
material_id = 'MAT-A'
quantity = 5 )
( %cid = 'ITM_002'
item_position = '0020'
material_id = 'MAT-B'
quantity = 3 ) ) )
).
MODIFY ENTITIES OF zi_sales_order
ENTITY SalesOrder
CREATE FIELDS ( customer_id order_date currency_code )
WITH orders_to_create
CREATE BY \_Item
FIELDS ( item_position material_id quantity )
WITH items_to_create
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
실전 예제 2단계: OData 페이로드 매핑과 오류 수집
실제 운영에서는 외부 시스템이 보낸 JSON 페이로드를 받아 Deep Create를 수행하는 경우가 많습니다. OData v4 클라이언트는 다음과 같은 페이로드를 보냅니다.
{
"CustomerId": "10000042",
"OrderDate": "2026-06-17",
"CurrencyCode": "EUR",
"_Item": [
{ "ItemPosition": "0010", "MaterialId": "MAT-A", "Quantity": 5 },
{ "ItemPosition": "0020", "MaterialId": "MAT-B", "Quantity": 3 }
]
}
이 페이로드를 ABAP에서 받아 %cid를 동적으로 부여하고, 실패 시 상세 메시지를 수집하는 패턴은 다음과 같습니다.
METHOD create_sales_order_deep.
DATA(lv_parent_cid) = |SO_{ cl_system_uuid=>create_uuid_c22_static( ) }|.
DATA(lt_orders) = VALUE TABLE FOR CREATE zi_sales_order(
( %cid = lv_parent_cid
customer_id = is_payload-customer_id
order_date = is_payload-order_date
currency_code = is_payload-currency_code ) ).
DATA lt_items TYPE TABLE FOR CREATE zi_sales_order\_Item.
LOOP AT is_payload-items INTO DATA(ls_item).
APPEND VALUE #(
%cid_ref = lv_parent_cid
%target = VALUE #(
( %cid = |ITM_{ sy-tabix WIDTH = 4 ALIGN = RIGHT PAD = '0' }|
item_position = ls_item-item_position
material_id = ls_item-material_id
quantity = ls_item-quantity ) )
) TO lt_items.
ENDLOOP.
MODIFY ENTITIES OF zi_sales_order
ENTITY SalesOrder
CREATE FIELDS ( customer_id order_date currency_code ) WITH lt_orders
CREATE BY \_Item FIELDS ( item_position material_id quantity ) WITH lt_items
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
IF ls_failed-salesorder IS NOT INITIAL OR ls_failed-salesorderitem IS NOT INITIAL.
LOOP AT ls_reported-salesorder INTO DATA(ls_rep).
log->add_message(
iv_severity = 'E'
iv_cid = ls_rep-%cid
iv_message = ls_rep-%msg->if_message~get_text( ) ).
ENDLOOP.
RAISE EXCEPTION TYPE zcx_sales_order_create
EXPORTING failed = ls_failed.
ENDIF.
COMMIT ENTITIES RESPONSE OF zi_sales_order
FAILED DATA(ls_commit_failed)
REPORTED DATA(ls_commit_reported).
rs_result-sales_order_id = VALUE #( ls_mapped-salesorder[ %cid = lv_parent_cid ]-salesorderid OPTIONAL ).
ENDMETHOD.
실전 예제 3단계: 동시성·테스트·보안 고려
운영 환경에서는 단순 매핑을 넘어 다음 항목까지 고려해야 합니다.
- 키 충돌 방지:
%cid문자열은 호출 단위로 유일해야 합니다. 외부에서 받은 값을 그대로 사용하지 말고, 서버 측에서cl_system_uuid기반으로 재생성하는 것을 권장합니다. - 권한 검증: BDEF의
authorization master ( instance )또는 글로벌 권한 체크가 Deep Create에서도 적용되도록, 부모 생성 권한과 자식 생성 권한을 모두 명시합니다. - 단위 테스트:
cl_abap_unit_assert와cl_cds_test_environment로 매핑 결과를 검증합니다.
%cid 누락 시 실제로 일어나는 일과 자주 묻는 질문
이 글의 핵심 앵글인 "%cid 없이 시도하면 어떻게 되는가"를 정리합니다.
- 부모
%cid누락: BDEF의 키 필드가 read-only인 경우,%cid가 없으면 RAP가 인스턴스 자체를 식별할 수 없어FAILED-%fail-cause = unspecific으로 떨어집니다. - 자식
%cid_ref누락: 자식이 부모의 신규 키를 직접 채우려 하면, 그 시점엔 부모 키가 비어 있어BUSINESS_DATA_INCONSISTENT오류가 발생합니다. - 중복
%cid: 같은 호출에서 두 부모에 동일한%cid를 부여하면, 자식이 잘못된 부모에 붙는 데이터 정합성 문제가 생깁니다.
Q. %cid는 트랜잭션 종료 후에도 유지되나요?
유지되지 않습니다. COMMIT ENTITIES 이후에는 MAPPED 구조에서 한 번 확인할 수 있을 뿐, 데이터베이스에는 저장되지 않습니다.
이 글 다음으로 살펴볼 주제
%cid를 익혔다면, RAP의 Draft 시나리오에서 %is_draft와 결합해 임시 인스턴스를 어떻게 저장·복원하는지, 그리고 Action에서 %cid를 입력 파라미터로 받아 신규 인스턴스에 액션을 즉시 실행하는 패턴을 살펴보면 좋습니다.
댓글 0
아직 댓글이 없습니다.