개요와 이 글에서 다루는 것
CDS(Core Data Services)의 Association은 ABAP 개발자가 데이터 모델 간 관계를 선언적으로 정의할 수 있게 해주는 강력한 기능입니다. 겉보기에는 SQL JOIN과 비슷해 보이지만, 실제 동작 방식은 완전히 다릅니다. 이 글에서는 Association의 lazy evaluation 원리, ON 조건 작성법, 그리고 PATH EXPRESSION과 EXPOSE 두 가지 활용 방식을 SalesOrder ↔ SalesOrderItem 관계를 통해 살펴봅니다.
- Association과 JOIN의 근본적 차이점 이해
- Cardinality(카디널리티) 표기법 [1..*], [0..1] 해석
- ON condition 작성 규칙과 자주 발생하는 문법 오류
- PATH EXPRESSION으로 Association 소비하기
- Consumer View에 Association 재노출(expose)하는 방법
- 성능 관점에서 Association이 "무료"인 이유
알고 있으면 좋은 배경
이 글은 ABAP CDS View 작성 경험이 있는 개발자를 대상으로 합니다. DEFINE VIEW 문법과 @AbapCatalog.sqlViewName, @AccessControl.authorizationCheck 같은 기본 어노테이션을 이미 사용해봤다면 무리 없이 따라올 수 있습니다. Open SQL의 INNER JOIN, LEFT OUTER JOIN 개념과 데이터베이스의 외래 키(Foreign Key) 원리도 함께 알고 있으면 도움이 됩니다.
환경과 버전, 준비물
실습에 사용된 환경은 다음과 같습니다. 버전에 따라 지원되는 어노테이션과 문법이 조금씩 다르니 참고하세요.
- SAP NetWeaver 7.52 이상 또는 SAP S/4HANA 1809 이상 (ABAP CDS Association 완전 지원)
- ABAP Development Tools(ADT) 3.20 이상이 설치된 Eclipse
- HANA DB 또는 AnyDB (일부 어노테이션은 HANA 전용이므로 SQL Trace로 실제 실행 계획 확인 권장)
- 테스트용 DDIC 테이블:
ZTSO_HEADER(주문 헤더),ZTSO_ITEM(주문 항목),ZTCUSTOMER(고객) — 실제 실습 시 커스텀 네임스페이스로 생성 - SE11에서 위 테이블에 최소한의 샘플 데이터 5~10건 입력
Eclipse ADT의 Project Explorer에서 대상 시스템에 연결한 뒤 File > New > Other > ABAP > Data Definition으로 View를 생성하는 흐름을 따릅니다.
핵심 개념 파고들기
Association을 이해하는 가장 좋은 비유는 "명함"입니다. SQL JOIN이 두 사람을 즉시 만나게 해서 대화를 시키는 것이라면, Association은 상대방의 명함을 미리 받아두는 것과 같습니다. 명함이 있어도 실제로 전화를 걸기 전까지는 아무 통신 비용이 발생하지 않습니다. 즉, Association은 관계를 선언만 할 뿐, 실제 데이터를 가져오는 조인 연산은 소비자가 그 필드를 실제로 요청할 때만 일어납니다. 이 방식을 lazy evaluation이라고 부릅니다.
일반적인 JOIN을 사용하면 SELECT 리스트에 조인 대상 필드를 쓰지 않아도 데이터베이스는 조인을 수행합니다. 반면 Association은 아래처럼 동작합니다.
Association을 정의만 하고 SELECT 절이나 WHERE 절에서 참조하지 않으면, 생성되는 SQL에는 해당 조인이 아예 포함되지 않습니다. 이는 뷰의 재사용성을 크게 높이는 동시에 불필요한 조인 비용을 절약합니다.
Cardinality 표기는 [min..max] 형태로 씁니다. [0..1]은 대응 데이터가 없거나 하나(to-one, LEFT OUTER 성격), [1..1]은 반드시 하나(INNER JOIN 성격), [1..*]는 하나 이상 다수(to-many)를 뜻합니다. 다만 카디널리티는 선언일 뿐 실제 데이터 정합성을 강제하지는 않습니다. 옵티마이저 힌트에 가깝다고 이해하는 편이 안전합니다.
@ObjectModel.association.type 어노테이션은 관계의 의미론을 지정합니다. 주요 값으로는 [#TO_COMPOSITION_PARENT](하위 항목에서 상위로), [#TO_COMPOSITION_CHILD](헤더에서 아이템으로), [#TO_COMPOSITION_ROOT](BOPF 루트 지시) 등이 있으며, BOPF나 RAP에서 트랜잭션 처리 시 참조됩니다. 조회 전용 뷰에서는 이 어노테이션이 없어도 Association 자체는 정상 동작합니다.
실전 코드 1단계 — 기본 Association 정의
가장 단순한 형태로 시작합니다. 주문 헤더(ZTSO_HEADER)에서 고객 마스터(ZTCUSTOMER)로 연결하는 to-one Association을 만듭니다.
@AbapCatalog.sqlViewName: 'ZVSO_HDR_BASE'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order Header - Base with Assoc'
define view ZC_SalesOrderHeaderBase
as select from ztso_header as hdr
association [0..1] to ZC_Customer as _Customer
on hdr.customer_id = _Customer.customer_id
{
key hdr.order_id as OrderId,
hdr.order_date as OrderDate,
hdr.customer_id as CustomerId,
hdr.total_amount as TotalAmount,
hdr.currency_code as CurrencyCode,
_Customer // expose association
}
여기서 눈여겨볼 점은 두 가지입니다. 첫째, Association 이름 앞에 언더스코어(_Customer)를 붙이는 것은 SAP 커뮤니티의 관행입니다. 필수는 아니지만 필드와 Association을 시각적으로 구분하기 위해 널리 쓰입니다. 둘째, 필드 리스트 마지막에 _Customer를 넣은 것이 바로 expose입니다. 이 한 줄이 없으면 이 뷰를 사용하는 상위 뷰에서 _Customer를 참조할 수 없습니다.
실전 코드 2단계 — 다중 Association과 PATH EXPRESSION
실무에서는 헤더 하나에 여러 관계가 얽힙니다. 주문 아이템, 배송지, 담당자 등입니다. 이번에는 아이템 뷰를 만들고, 헤더 뷰에서 아이템으로의 to-many Association을 추가합니다. 그리고 Consumer 뷰에서 PATH EXPRESSION으로 관련 필드를 꺼내오는 방식을 보여줍니다.
@AbapCatalog.sqlViewName: 'ZVSO_ITM_BASE'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order Item - Base'
define view ZC_SalesOrderItemBase
as select from ztso_item as itm
association [1..1] to ZC_SalesOrderHeaderBase as _Header
on itm.order_id = _Header.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,
_Header
}
이제 헤더 뷰에 to-many Association을 추가하고, 아이템 개수와 고객명을 함께 조회하는 Consumer 뷰를 만듭니다.
@AbapCatalog.sqlViewName: 'ZVSO_HDR_ENR'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Header - Enriched'
define view ZC_SalesOrderHeaderEnriched
as select from ZC_SalesOrderHeaderBase as hdr
association [0..*] to ZC_SalesOrderItemBase as _Items
on hdr.OrderId = _Items.OrderId
{
key hdr.OrderId,
hdr.OrderDate,
hdr.TotalAmount,
hdr.CurrencyCode,
hdr._Customer.customer_name as CustomerName,
hdr._Customer.country_code as CustomerCountry,
count( distinct _Items.ItemNo ) as ItemCount,
_Items,
hdr._Customer
}
group by
hdr.OrderId,
hdr.OrderDate,
hdr.TotalAmount,
hdr.CurrencyCode,
hdr._Customer.customer_name,
hdr._Customer.country_code
실전 코드 3단계 — 프로덕션 관점의 필터, 파라미터, 성능
운영 환경에서는 권한 체크, 파라미터, 필터 조건이 뒤엉키기 때문에 Association 사용 시 주의가 필요합니다. 아래는 특정 통화와 최소 금액을 파라미터로 받아 활성 주문만 노출하는 프로덕션급 뷰입니다.
@AbapCatalog.sqlViewName: 'ZVSO_ANLY'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@Analytics.dataCategory: #CUBE
@EndUserText.label: 'Sales Order Analytics'
define view ZC_SalesOrderAnalytics
with parameters
p_currency : abap.cuky,
p_min_amount : abap.dec( 15, 2 )
as select from ZC_SalesOrderHeaderEnriched as hdr
association [0..*] to ZC_SalesOrderItemBase as _Items
on hdr.OrderId = _Items.OrderId
{
key hdr.OrderId,
hdr.OrderDate,
hdr.CustomerName,
hdr.CustomerCountry,
@Semantics.amount.currencyCode: 'CurrencyCode'
hdr.TotalAmount,
hdr.CurrencyCode,
hdr.ItemCount,
case
when hdr.ItemCount >= 10 then 'LARGE'
when hdr.ItemCount >= 3 then 'MEDIUM'
else 'SMALL'
end as OrderSize,
_Items
}
where hdr.CurrencyCode = :p_currency
and hdr.TotalAmount >= :p_min_amount
자주 걸리는 함정과 해결 방법
Q1. Association을 정의했는데 상위 뷰에서 인식하지 못합니다.
필드 리스트에 Association을 노출하지 않은 경우가 가장 흔한 원인입니다. _Customer처럼 필드 리스트에 이름 한 줄을 넣어 expose해야 상위 뷰가 참조할 수 있습니다.
Q2. Cardinality를 [1..1]로 선언했는데 데이터가 없는 행에서 결과가 사라집니다.
카디널리티는 선언일 뿐이지만, 옵티마이저가 INNER JOIN처럼 처리할 수 있습니다. null 가능성이 있다면 [0..1]로 선언하세요.
Q3. ON condition에 상수 비교나 함수 호출을 넣으면 오류가 납니다.
Association ON 절은 일반 JOIN보다 제약이 강합니다. 복잡한 조건이 필요하면 하위 뷰에서 미리 필터한 파생 뷰를 만들고 그 뷰에 Association을 거세요.
Q4. Association 필드가 SELECT에 없는데도 SQL에 조인이 보입니다.
WHERE 절, GROUP BY, ORDER BY에서 Association 필드를 참조하거나, 상위 뷰에서 재노출된 Association을 Consumer가 참조하면 체인 전체의 조인이 실행됩니다. ST05 트레이스로 확인하세요.
성능 분석 및 모니터링
Association 기반 뷰를 프로덕션에 적용할 때는 반드시 실제 실행 계획을 확인해야 합니다. SAP 시스템에서 제공하는 ST05(SQL Trace)를 사용하면 CDS View가 내부적으로 생성하는 실제 SQL을 캡처할 수 있습니다. Association이 lazy evaluation으로 작동한다는 이론과 실제 데이터베이스가 만들어낸 실행 계획이 다를 수 있기 때문입니다.
ST05 사용법은 간단합니다. SE30 또는 ST05 트랜잭션을 열고 "SQL Trace"를 시작한 뒤, 대상 프로그램이나 Fiori 앱에서 조회를 실행합니다. 트레이스를 종료하고 "Display Trace"를 클릭하면 실행된 모든 SQL이 시간 순으로 나타납니다. "ABAP" 필터로 줄이면 CDS View가 만들어낸 SELECT 문만 볼 수 있습니다. 이 SQL에 JOIN 대상 테이블이 나타나는지를 확인하는 것이 핵심입니다.
HANA 환경이라면 SAP HANA Studio의 Plan Visualizer 또는 HANA Cockpit의 SQL Analyzer를 추가로 활용할 수 있습니다. 특히 대용량 테이블에 to-many Association을 걸 경우, 집계 뷰 단계에서 카운트 연산이 HANA 내부 pushdown으로 처리되는지 확인하면 성능 차이가 극명하게 드러납니다.
이어서 살펴볼 만한 주제
Association 기초를 익혔다면 자연스러운 확장 주제는 RAP(RESTful Application Programming Model)의 Composition입니다. Composition은 Association의 특수한 형태로, 부모-자식 라이프사이클을 함께 관리합니다. 또한 CDS View Entity(신형 문법)에서는 @AbapCatalog.sqlViewName이 사라지고 문법이 단순해졌으므로, S/4HANA 최신 릴리스에서는 View Entity 기반으로 재작성하는 것을 권장합니다. DCL(Data Control Language)을 통한 인스턴스 레벨 권한 제어, ABAP Managed Database Procedures(AMDP)와의 결합도 자연스러운 다음 단계입니다.
댓글 0
아직 댓글이 없습니다.