RAP

RAP Composition 실수 3가지 — 계층 BO 설계 #shorts #SAP #RAP

RAP Composition Tree란 무엇인가 — 계층 BO의 설계 원칙

SAP RAP(ABAP RESTful Application Programming Model)에서 비즈니스 오브젝트(BO)는 단일 엔티티가 아니라 트리 구조로 설계됩니다. 판매 오더를 떠올려 보면 이해가 쉽습니다. 오더 헤더가 있고, 그 아래에 여러 개의 아이템이 매달려 있습니다. 헤더가 삭제되면 아이템도 함께 사라져야 하고, 아이템은 헤더 없이 단독으로 존재할 수 없습니다. 이런 생명주기 종속 관계를 RAP에서는 Composition으로 표현합니다.

Composition Tree의 핵심 원칙은 세 가지입니다.

  • 단일 Root: 트리에는 반드시 하나의 Root 엔티티만 존재하며, 모든 트랜잭션 요청(Create/Update/Delete)은 Root를 통해 진입합니다.
  • 양방향 연결: 부모는 자식을 composition으로, 자식은 부모를 association to parent로 가리켜야 합니다. 한쪽만 선언하면 활성화 자체가 실패합니다.
  • 계층 상속: Draft, Lock, Authorization 같은 트랜잭션 속성은 일반적으로 Root에서 정의하고 자식이 이를 따라갑니다.

이 글은 SAP BTP ABAP Environment 및 S/4HANA 2022 이상(ABAP Platform 2022+)을 기준으로, 헤더-아이템 관계를 BDEF로 정의할 때 실무에서 가장 자주 발생하는 실수 3가지와 그 해결 방법을 다룹니다. 개발 도구는 ADT(ABAP Development Tools for Eclipse)를 사용합니다.

BDEF에서 Composition 선언 — 올바른 구문과 역할

Composition은 두 곳에서 선언됩니다. 먼저 CDS View 레벨에서 관계를 정의하고, 그다음 BDEF(Behavior Definition)에서 트랜잭션 동작을 부여합니다. 이 둘의 방향과 이름이 일치하지 않으면 계층이 성립하지 않습니다.

부모 CDS View에서는 composition 키워드를 사용합니다.

define root view entity ZI_SalesOrder
  as select from zso_header
  composition [0..*] of ZI_SalesOrderItem as _Item
{
  key sales_order_uuid   as SalesOrderUuid,
      sales_order_id     as SalesOrderId,
      buyer_id           as BuyerId,
      total_amount       as TotalAmount,
      local_last_changed as LocalLastChangedAt,
      _Item -- 자식으로의 composition 노출
}

자식 CDS View에서는 반대 방향으로 association to parent를 선언합니다.

define view entity ZI_SalesOrderItem
  as select from zso_item
  association to parent ZI_SalesOrder as _SalesOrder
    on $projection.SalesOrderUuid = _SalesOrder.SalesOrderUuid
{
  key item_uuid          as ItemUuid,
      sales_order_uuid   as SalesOrderUuid,
      product_id         as ProductId,
      quantity           as Quantity,
      local_last_changed as ItemLastChangedAt,
      _SalesOrder
}

BDEF에서는 부모 쪽에 composition [create] 대신 association 제어 구문으로 자식 생성 권한을 부여합니다. 핵심은 자식 인스턴스의 생성이 항상 부모를 경유(create by association)한다는 점입니다. 자식 엔티티에 직접 create;를 선언하는 것은 Composition 모델에서 허용되지 않습니다.

실수 ①: Root BO와 Child BO의 키 매핑 오류

가장 흔하면서도 발견하기 어려운 실수입니다. 판매 오더에는 보통 두 종류의 식별자가 있습니다. 내부 기술 키인 SalesOrderUuid(RAW16)와 사용자에게 보이는 시맨틱 키인 SalesOrderId(예: "SO-2026-0001")입니다. Composition의 ON 조건에 어느 쪽을 쓰는지에 따라 결과가 완전히 달라집니다.

잘못된 예를 보겠습니다.

-- 잘못된 매핑: 시맨틱 키로 부모-자식 연결
association to parent ZI_SalesOrder as _SalesOrder
  on $projection.SalesOrderId = _SalesOrder.SalesOrderId

이렇게 하면 세 가지 문제가 발생합니다.

  1. Draft 시나리오 붕괴: 신규 Draft 인스턴스는 아직 시맨틱 번호(SalesOrderId)가 채번되지 않은 상태입니다. UUID로 연결하지 않으면 Draft 상태의 아이템이 부모를 찾지 못합니다.
  2. 키 변경 시 고아 레코드: 시맨틱 키가 수정될 수 있는 구조라면 자식이 통째로 끊어집니다.
  3. 성능 저하: 문자열 키 조인은 RAW16 UUID 조인보다 일반적으로 비용이 큽니다.

권장 패턴은 기술 키(UUID)로만 계층을 연결하고, 시맨틱 키는 표시용으로 분리하는 것입니다. Managed 시나리오에서는 field ( numbering : managed, readonly ) SalesOrderUuid;로 UUID 자동 채번을 맡기고, 시맨틱 번호는 determination에서 채번하는 구성이 일반적입니다.

실수 ②: Association to Parent 누락 또는 방향 역전

부모 View에 composition of ... as _Item만 선언하고 자식 View에 association to parent를 빠뜨리는 경우가 두 번째 실수입니다. 이때 활성화 시점에 다음과 같은 유형의 오류를 만나게 됩니다.

"The entity ZI_SALESORDERITEM is used as a composition child but does not contain an association to its parent."

반대로 방향을 역전시키는 실수도 있습니다. 자식 쪽에 일반 association [1..1] to ZI_SalesOrder를 선언하는 경우인데, 이는 문법상 통과할 수 있어도 RAP 런타임이 이를 부모 관계로 인식하지 못합니다. 반드시 to parent 키워드가 있어야 Composition Tree의 일원으로 등록됩니다. 일반 association은 단순 참조(예: 아이템에서 제품 마스터 조회)에만 사용해야 합니다.

BDEF 쪽에서도 대칭 선언이 필요합니다. 부모에는 association _Item { create; }, 자식에는 association _SalesOrder;를 함께 명시해야 Fiori Elements Object Page에서 아이템 테이블의 Create 버튼과 부모 내비게이션이 모두 동작합니다. 한쪽이라도 빠지면 UI에서 버튼이 비활성화되거나, OData 요청 시 Deep Create not supported 계열 오류가 발생할 수 있습니다.

// 부모 BDEF 내부 — 자식 생성 경로 개방
define behavior for ZI_SalesOrder alias SalesOrder
{
  ...
  association _Item { create; with draft; }
}

// 자식 BDEF 내부 — 부모로의 역방향 선언
define behavior for ZI_SalesOrderItem alias SalesOrderItem
{
  ...
  association _SalesOrder { with draft; }
}

실수 ③: Etag와 Draft Handling 계층 불일치

세 번째 실수는 트랜잭션 속성의 계층 배치를 잘못하는 것입니다. 두 가지를 구분해야 합니다.

  • Draft: Root에만 선언with draft;는 Root 엔티티 정의부에 한 번만 붙입니다. 자식에는 draft table 지정만 하면 되고, Draft 활성화 자체를 자식에 다시 선언하면 오류입니다. 대신 자식으로 향하는 association과 부모로 향하는 association 양쪽에 with draft;를 붙여 Draft 상태에서의 탐색을 열어 주어야 합니다.
  • Etag: 계층별 역할이 다름 — Root에는 total etag(트리 전체의 변경 감지, 일반적으로 헤더의 LastChangedAt), 각 엔티티에는 etag master(해당 엔티티 자신의 낙관적 잠금, 보통 LocalLastChangedAt)를 선언합니다.

흔한 오배치 사례는 자식 엔티티의 etag master부모의 필드를 지정하거나, 자식에 자체 lastChangedAt 필드 없이 etag를 생략하는 경우입니다. 전자는 아이템 하나만 수정해도 다른 아이템의 편집이 412(Precondition Failed)로 거부되는 과잉 잠금을, 후자는 동시 수정 충돌을 감지하지 못하는 상태를 만듭니다. 각 자식 테이블에 abp_locinst_lastchange_tstmpl 타입의 자체 타임스탬프 필드를 두고, 헤더에는 트리 전체용 abp_lastchange_tstmpl 필드를 추가로 두는 이중 구조가 권장됩니다.

올바른 헤더-아이템 BDEF 설계 실전 예제 (SalesOrder / SalesOrderItem)

앞의 세 가지 실수를 모두 반영해 교정한 Managed + Draft 시나리오의 전체 BDEF입니다.

managed implementation in class zbp_i_salesorder unique;
strict ( 2 );
with draft;

define behavior for ZI_SalesOrder alias SalesOrder
persistent table zso_header
draft table zso_header_d
lock master total etag LastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
{
  create;
  update;
  delete;

  field ( numbering : managed, readonly ) SalesOrderUuid;
  field ( readonly ) SalesOrderId, TotalAmount,
                     LastChangedAt, LocalLastChangedAt;

  association _Item { create; with draft; }

  determination calculateOrderId on save
    { create; }
  determination calculateTotal on modify
    { field TotalAmount; }

  draft action Activate optimized;
  draft action Discard;
  draft action Edit;
  draft action Resume;
  draft determine action Prepare;

  mapping for zso_header corresponding;
}

define behavior for ZI_SalesOrderItem alias SalesOrderItem
persistent table zso_item
draft table zso_item_d
lock dependent by _SalesOrder
authorization dependent by _SalesOrder
etag master ItemLastChangedAt
{
  update;
  delete;

  field ( numbering : managed, readonly ) ItemUuid;
  field ( readonly ) SalesOrderUuid;

  association _SalesOrder { with draft; }

  validation validateQuantity on save
    { field Quantity; create; }

  mapping for zso_item corresponding;
}

주목할 포인트를 정리하면 다음과 같습니다.

  • 자식에는 create;가 없습니다. 아이템 생성은 부모의 association _Item { create; }를 통해서만 가능합니다.
  • lock dependent by _SalesOrder, authorization dependent by _SalesOrder — 잠금과 권한을 Root에 위임해 트리 전체가 하나의 트랜잭션 단위로 움직입니다.
  • SalesOrderUuid는 자식에서 readonly입니다. 부모 키는 프레임워크가 채워 주므로 외부에서 조작할 수 없게 막습니다.
  • strict ( 2 ) 모드를 사용하면 ADT가 계층 관련 문법 오류를 활성화 시점에 더 엄격하게 잡아 줍니다.

Activation 오류 패턴과 디버깅 방법

Composition 관련 활성화 오류는 메시지만 잘 읽어도 원인 계층을 특정할 수 있습니다. 자주 만나는 패턴과 점검 순서입니다.

오류 패턴주요 원인점검 위치
"...does not contain an association to its parent"자식 View의 to parent 누락자식 CDS View
"Operation CREATE is not allowed for entity ..."자식에 직접 create 선언 또는 부모 association에 create 누락부모/자식 BDEF
"Draft table ... missing"Draft 시나리오에서 자식 draft table 미지정자식 BDEF
412 Precondition Failed 빈발etag master 필드 오배치양쪽 BDEF의 etag 선언
Deep Create 시 자식 미생성ON 조건 키 불일치(UUID vs 시맨틱 키)자식 View의 ON 조건

디버깅 시에는 ADT의 EML(Entity Manipulation Language) 콘솔 클래스로 UI를 배제하고 계층 동작만 검증하는 방법이 효과적입니다.

MODIFY ENTITIES OF ZI_SalesOrder
  ENTITY SalesOrder
    CREATE FIELDS ( BuyerId ) WITH VALUE #(
      ( %cid = 'H1' BuyerId = 'ACME' ) )
    CREATE BY \_Item FIELDS ( ProductId Quantity ) WITH VALUE #(
      ( %cid_ref = 'H1'
        %target  = VALUE #(
          ( %cid = 'I1' ProductId = 'MAT-100' Quantity = 5 ) ) ) )
  MAPPED   DATA(mapped)
  FAILED   DATA(failed)
  REPORTED DATA(reported).

failed 구조의 %fail-cause를 확인하면 dependency(부모 미해결), unauthorized 등 실패 원인이 계층 관점에서 드러납니다. 그 외에 ADT의 Behavior Definition 에디터에서 엔티티명을 Ctrl+클릭해 CDS와 BDEF의 alias·association 이름이 서로 일치하는지 교차 확인하는 습관도 유용합니다.

RAP Composition 계층 설계 체크리스트와 더 살펴볼 주제

배포 전 아래 항목을 점검하면 이 글에서 다룬 실수 대부분을 예방할 수 있습니다.

  1. 부모 View에 composition [0..*] of, 자식 View에 association to parent가 쌍으로 존재하는가?
  2. ON 조건이 UUID 기술 키로만 연결되어 있는가? (시맨틱 키 연결 금지)
  3. 자식 BDEF에 직접 create;가 없고, 부모의 association _Item { create; }로만 생성되는가?
  4. with draft;는 Root에만 있고, 양방향 association에 with draft;가 붙어 있는가?
  5. Root에 total etag + 엔티티별 etag master가 각자의 타임스탬프 필드를 가리키는가?
  6. 자식의 lock/authorization이 dependent by _SalesOrder로 위임되어 있는가?
  7. EML 테스트 클래스로 Deep Create가 검증되었는가?

이 구조가 익숙해지면 다음 주제로 확장해 보시길 권합니다. 3단계 이상 계층(헤더-아이템-스케줄라인)의 lock dependent 체인, Projection View와 Behavior Projection에서의 계층 노출 제어, 그리고 Draft 상태에서의 side effects 선언이 자연스러운 다음 학습 지점입니다.

댓글 0

아직 댓글이 없습니다.