RAP

%cid 30초 이해 — 임시 키로 부모 참조하는 법 #shorts #SAP #RAP

▶ YouTube에서 보기

이 글에서 다루는 내용과 도달 지점

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_assertcl_cds_test_environment로 매핑 결과를 검증합니다.

%cid 누락 시 실제로 일어나는 일과 자주 묻는 질문

이 글의 핵심 앵글인 "%cid 없이 시도하면 어떻게 되는가"를 정리합니다.

  1. 부모 %cid 누락: BDEF의 키 필드가 read-only인 경우, %cid가 없으면 RAP가 인스턴스 자체를 식별할 수 없어 FAILED-%fail-cause = unspecific으로 떨어집니다.
  2. 자식 %cid_ref 누락: 자식이 부모의 신규 키를 직접 채우려 하면, 그 시점엔 부모 키가 비어 있어 BUSINESS_DATA_INCONSISTENT 오류가 발생합니다.
  3. 중복 %cid: 같은 호출에서 두 부모에 동일한 %cid를 부여하면, 자식이 잘못된 부모에 붙는 데이터 정합성 문제가 생깁니다.

Q. %cid는 트랜잭션 종료 후에도 유지되나요?
유지되지 않습니다. COMMIT ENTITIES 이후에는 MAPPED 구조에서 한 번 확인할 수 있을 뿐, 데이터베이스에는 저장되지 않습니다.

이 글 다음으로 살펴볼 주제

%cid를 익혔다면, RAP의 Draft 시나리오에서 %is_draft와 결합해 임시 인스턴스를 어떻게 저장·복원하는지, 그리고 Action에서 %cid를 입력 파라미터로 받아 신규 인스턴스에 액션을 즉시 실행하는 패턴을 살펴보면 좋습니다.

댓글 0

아직 댓글이 없습니다.