개요와 이 글에서 얻어갈 것
ABAP CDS와 RAP(RESTful ABAP Programming Model)를 다루다 보면 association to와 composition of 두 키워드를 매일 마주치게 됩니다. 문법은 비슷해 보이지만, 두 관계는 라이프사이클(생성/수정/삭제) 관리 방식에서 근본적으로 다릅니다. 이 차이를 잘못 이해하면 RAP BO(Business Object) 설계 자체가 뒤틀리고, Draft 처리·권한 체크·저장 로직까지 연쇄적으로 꼬입니다.
이 글에서 다음을 정리합니다.
- Association과 Composition의 개념적 차이와 CDS 문법상 차이
- RAP에서 Composition이 부모-자식 라이프사이클을 어떻게 강제하는지
- SalesOrder / SalesOrderItem / Customer 시나리오로 본 실전 모델링
- Behavior Definition에서 두 관계가 만들어내는 동작 차이
- 실무에서 흔히 하는 오해와 트러블슈팅 포인트
체크리스트: (1) 두 관계 중 어느 것이 외래키만 갖는지 (2) Composition Root의 의미 (3) Draft-Enabled 시 자식 엔티티 처리 방식 (4) Cascade Delete가 언제 자동 발생하는지 — 이 네 가지를 글을 읽고 나서 스스로 설명할 수 있어야 합니다.
이 글을 편하게 읽기 위한 배경 지식
ABAP CDS View / CDS Entity 문법에 대한 기본 이해가 필요합니다. define view entity, @ObjectModel 어노테이션, projection view의 개념을 접해봤다면 충분합니다. RAP의 BO(Business Object) 구조 — Root Entity와 Child Entity, Behavior Definition, Behavior Implementation Class — 를 한 번이라도 만져봤다면 더 빠르게 이해할 수 있습니다. SQL의 Foreign Key와 ON DELETE CASCADE 개념도 도움이 됩니다.
환경과 준비물
이 글의 예제는 다음 환경을 기준으로 작성했습니다. 다른 릴리스에서도 개념은 동일하나, 문법 일부(예: with additional save, numbering 옵션)는 릴리스에 따라 지원 범위가 다를 수 있습니다.
- SAP BTP, ABAP Environment (Steampunk) 2025 릴리스 또는 SAP S/4HANA 2023 On-Premise
- ABAP Development Tools (ADT) for Eclipse 2025-03 이상
- RAP Managed / Unmanaged 시나리오 모두 대응 가능한 Package 권한
- Fiori Elements Preview를 열 수 있는 브라우저 (선택)
CDS 문법은 하위 릴리스와 어느 정도 호환되지만, RAP의 Draft 및 Composition 동작은 릴리스별로 개선이 계속되고 있으므로 도입 전 릴리스 노트 확인을 권장합니다.
핵심 개념: 참조인가, 소유인가
Association과 Composition을 한 문장으로 구분하면 이렇습니다.
Association은 "참조(reference)"이고, Composition은 "소유(ownership)"이다.
비유하자면 Association은 명함을 주고받는 관계입니다. 내가 상대의 명함(키)을 가지고 있지만, 상대가 사라져도 나는 계속 존재합니다. 반대로 Composition은 부모와 태아의 관계입니다. 부모가 없으면 자식은 존재할 이유가 없고, 부모가 삭제되면 자식도 함께 사라져야 합니다.
CDS 관점에서 정리하면 다음과 같습니다.
- Association (
association to): 두 독립 엔티티 간의 조인 정보를 정의합니다. 라이프사이클이 서로 독립적이고, DELETE 시 상대에 아무 영향도 주지 않습니다. 주로 마스터 데이터 참조(고객, 통화, 국가 등)에 사용합니다. - Composition (
composition of): 부모-자식 관계를 정의합니다. 자식 엔티티는 부모 없이는 존재할 수 없으며, RAP에서 Composition Tree를 구성해 하나의 트랜잭션 단위로 처리됩니다. - To-Parent Association: Composition의 자식 쪽에서 부모를 가리키는 역방향 관계입니다.
association to parent로 선언하며 Composition과 짝을 이룹니다.
도식으로 표현하면 이렇습니다.
[SalesOrder] ──association to──▶ [Customer] (독립: 고객 삭제해도 주문 유지)
│
└──composition of──▶ [SalesOrderItem] (종속: 주문 삭제 시 아이템도 자동 삭제)
│
└──association to parent──▶ [SalesOrder]
RAP에서 이 차이는 특히 중요합니다. Composition으로 묶인 엔티티들은 하나의 Business Object로 취급되어, Root의 Create/Update/Delete가 자식에게 그대로 전파됩니다. Draft 처리 시에도 부모의 Draft에 자식이 함께 담겨 저장되고, 활성화(Activation) 역시 트리 전체가 한 번에 처리됩니다.
실전 예제 1단계: 순수 CDS로 두 관계 정의하기
먼저 데이터베이스 테이블 세 개가 있다고 가정합니다: zt_sord_hdr(주문 헤더), zt_sord_itm(주문 아이템), zt_customer(고객). 이제 CDS Entity로 관계를 표현해봅니다.
// 고객 마스터 (독립 엔티티)
@AccessControl.authorizationCheck: #NOT_REQUIRED
define view entity ZI_Customer
as select from zt_customer
{
key customer_id as CustomerId,
customer_name as CustomerName,
country_code as CountryCode,
credit_limit as CreditLimit
}
// 주문 헤더 (Composition Root)
@AccessControl.authorizationCheck: #NOT_REQUIRED
define root view entity ZI_SalesOrder
as select from zt_sord_hdr as Hdr
composition [0..*] of ZI_SalesOrderItem as _Items
association [1..1] to ZI_Customer as _Customer
on $projection.CustomerId = _Customer.CustomerId
{
key Hdr.order_id as OrderId,
Hdr.customer_id as CustomerId,
Hdr.order_date as OrderDate,
Hdr.total_amount as TotalAmount,
Hdr.currency_code as CurrencyCode,
_Items,
_Customer
}
// 주문 아이템 (자식 엔티티)
@AccessControl.authorizationCheck: #NOT_REQUIRED
define view entity ZI_SalesOrderItem
as select from zt_sord_itm as Itm
association to parent ZI_SalesOrder as _Order
on $projection.OrderId = _Order.OrderId
{
key Itm.order_id as OrderId,
key Itm.item_no as ItemNo,
Itm.material_id as MaterialId,
Itm.quantity as Quantity,
Itm.net_price as NetPrice,
_Order
}
핵심 관찰 포인트입니다.
ZI_SalesOrder는define root view entity로 선언되어 Composition Tree의 뿌리임을 밝힙니다.composition [0..*] of ZI_SalesOrderItem은 "이 주문은 여러 아이템을 소유한다"는 의미이며, 자식은 반드시 부모로부터 접근됩니다.ZI_SalesOrderItem의association to parent는 Composition의 짝으로 반드시 필요합니다. 없으면 활성화 시 오류가 발생합니다.- 고객과의 관계는
association입니다. 주문이 삭제된다고 해서 고객이 삭제되면 안 되고, 반대로도 마찬가지입니다.
실전 예제 2단계: RAP Behavior Definition에서의 차이
이제 같은 모델에 Behavior Definition을 얹으면 두 관계의 차이가 극명하게 드러납니다.
managed implementation in class zbp_i_salesorder unique;
strict ( 2 );
with draft;
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zt_sord_hdr
draft table zt_sord_hdr_d
lock master
total etag OrderDate
authorization master ( instance )
etag master OrderDate
{
create;
update;
delete;
field ( read only ) OrderId;
field ( mandatory ) CustomerId, OrderDate;
association _Items { create; with draft; }
association _Customer;
draft action Edit;
draft action Activate;
draft action Discard;
draft determine action Prepare;
}
define behavior for ZI_SalesOrderItem alias SalesOrderItem
persistent table zt_sord_itm
draft table zt_sord_itm_d
lock dependent by _Order
authorization dependent by _Order
etag master OrderDate
{
update;
delete;
field ( read only ) OrderId, ItemNo;
association _Order;
}
이 Behavior Definition에서 확인할 것들입니다.
- Composition 쪽 (
_Items)에는{ create; with draft; }를 붙여 부모로부터 자식을 생성할 수 있음을 선언합니다. 자식 엔티티 자체에는create를 쓰지 않습니다 — 자식은 항상 부모의 컨텍스트에서 생성됩니다. - 자식 엔티티의
lock dependent by _Order는 락(Lock)이 부모를 통해 걸린다는 뜻입니다. - 자식의
authorization dependent by _Order는 권한 체크 역시 부모에 위임됨을 의미합니다. - Association 쪽 (
_Customer)에는 그저association _Customer;만 선언합니다. Customer는 SalesOrder BO의 라이프사이클에 관여하지 않습니다.
이 구조에서 DELETE가 실행되면 어떤 일이 벌어지는지 관찰합니다.
MODIFY ENTITIES OF ZI_SalesOrder
ENTITY SalesOrder
DELETE FROM VALUE #( ( OrderId = '4500000001' %cid = 'C1' ) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
이 한 번의 삭제로 ZI_SalesOrderItem에 속한 모든 아이템이 자동으로 함께 삭제됩니다. Cascade Delete를 명시적으로 코딩할 필요가 없습니다. Composition이 곧 계약이기 때문입니다. 반면 _Customer는 그대로 남습니다.
실전 예제 3단계: Projection과 서비스 노출
실무에서는 Interface 계층의 CDS를 그대로 노출하지 않고, Projection View를 한 겹 씌워 UI Consumption에 맞춰 재구성합니다.
@Metadata.allowExtensions: true
@AccessControl.authorizationCheck: #CHECK
define root view entity ZC_SalesOrder
provider contract transactional_query
as projection on ZI_SalesOrder
{
key OrderId,
CustomerId,
OrderDate,
TotalAmount,
CurrencyCode,
_Customer : redirected to ZC_Customer,
_Items : redirected to composition child ZC_SalesOrderItem
}
@Metadata.allowExtensions: true
@AccessControl.authorizationCheck: #CHECK
define view entity ZC_SalesOrderItem
as projection on ZI_SalesOrderItem
{
key OrderId,
key ItemNo,
MaterialId,
Quantity,
NetPrice,
_Order : redirected to parent ZC_SalesOrder
}
주목할 문법입니다.
redirected to composition child: Projection에서도 Composition 관계를 유지하려면 자식이 어느 Projection Entity로 리다이렉트되는지 명시해야 합니다.redirected to parent: 자식의 to-parent도 Projection Root로 재연결합니다.redirected to ZC_Customer: 단순 Association은 대상 Projection Entity로만 리다이렉트합니다.
프로덕션 관점의 추가 고려사항입니다.
- 성능: Composition은 별도 read를 유발할 수 있습니다. Fiori List Report에서 아이템 개수를 표시하려면 aggregation view를 두어 N+1 문제를 피하는 것이 일반적으로 권장됩니다.
- 테스트: RAP BO Test Double Framework(
cl_abap_behv_test_double)로 Composition 단위 테스트를 작성할 수 있습니다. 자식은 부모 컨텍스트에서 생성되므로%cid_ref로 임시 참조를 연결하는 패턴을 사용합니다. - 보안: Association 대상의 권한은 대상 엔티티의 DCL에서 별도로 걸어야 합니다. Composition 자식은
authorization dependent by로 부모에게 위임할 수 있어 중복 정의를 피할 수 있습니다.
자주 겪는 실수와 트러블슈팅
Q1. Composition으로 잡았더니 활성화가 안 됩니다. "The association to parent is missing"이라는 오류가 뜹니다.
Composition은 반드시 자식 쪽에 association to parent가 짝으로 있어야 합니다. 자식 CDS에 to-parent를 추가하고, 조인 조건이 부모의 키 필드를 모두 포함하는지 확인해야 합니다. 특히 복합키(Composite Key)인 경우 조건 하나만 빠져도 활성화가 거부됩니다.
Q2. Customer를 Composition으로 잡으면 어떤 문제가 생기나요?
문법상 오류는 나지 않지만 의미가 완전히 뒤틀립니다. Composition으로 선언하는 순간 Customer는 SalesOrder의 소유물이 되어, 주문 삭제 시 고객까지 삭제 대상이 됩니다. 또한 Customer가 자기 자신의 Root BO를 가질 수 없게 되어, 고객 관리 화면 자체를 만들 수 없습니다. 마스터 데이터는 Association이 원칙입니다.
Q3. Draft-Enabled BO에서 자식만 저장하고 부모는 Draft 상태로 두고 싶은데 가능한가요?
불가능하고, 시도해서도 안 됩니다. Composition Tree는 단일 트랜잭션 단위입니다. Draft 활성화(Activate)는 트리 전체에 적용되며, 자식만 부분 커밋하는 개념 자체가 RAP 모델에 없습니다. 만약 그런 요구가 있다면 애초에 자식이 Composition이 아니라 별도 BO여야 한다는 신호입니다.
Q4. Association도 [1..1]처럼 카디널리티를 강하게 잡으면 사실상 부모-자식 아닌가요?
아닙니다. 카디널리티는 조인 결과의 행 수를 힌트로 알려줄 뿐, 라이프사이클과는 무관합니다. association [1..1]이라도 상대는 여전히 독립 엔티티이고, DELETE가 전파되지 않습니다. 라이프사이클 결합이 필요하면 반드시 composition을 써야 합니다.
Q5. Composition Root가 여러 개 있을 수 있나요?
하나의 Composition Tree에는 Root가 정확히 하나입니다. 여러 Root처럼 보인다면 그것은 서로 다른 BO 여러 개를 Association으로 연결한 것입니다. 이 개념적 구분을 유지해야 서비스 정의(Service Definition)에서 각 BO를 독립적으로 노출·소비할 수 있습니다.
확장 공부 주제와 참고 문서
Association과 Composition을 확실히 잡았다면 다음 주제로 넓혀가는 것을 권장합니다.
- Managed vs Unmanaged RAP: Composition의 라이프사이클을 프레임워크가 관리하느냐(Managed), 개발자가 직접 구현하느냐(Unmanaged)의 차이
- Late Numbering vs Early Numbering: 자식 키를 언제 부여하는지에 대한 전략, 특히 Composition에서 자주 문제되는 부분
- Foreign Key Relations와 CDS Semantic: Association에
@ObjectModel.foreignKey.association을 다는 이유 - Determinations와 Validations: Composition Tree 안에서 부모/자식 변경을 감지해 자동 계산을 트리거하는 방법
- Draft Handling 심화:
lock dependent,authorization dependent가 실제 락 테이블과 권한 체크 흐름에 미치는 영향
Association과 Composition의 차이는 문법 한 줄의 차이가 아니라 도메인 모델 설계의 차이입니다. "이 엔티티가 부모 없이 존재할 이유가 있는가?"라는 질문에 답할 수 있으면 이미 절반은 해결된 것이고, 나머지 절반은 RAP가 알아서 처리해줍니다.
댓글 0
아직 댓글이 없습니다.