개요 — 이 글이 답하는 질문
ABAP CDS(Core Data Services)는 단순한 데이터 조회 뷰를 넘어, 다차원 분석(OLAP)을 위한 의미 계층(Semantic Layer)을 ABAP 레이어에서 직접 구축할 수 있게 해줍니다. 그 중심에 @Analytics.dataCategory 어노테이션이 있으며, 값으로 #DIMENSION, #FACT, #CUBE 세 가지가 자주 사용됩니다. 세 값은 단순한 라벨이 아니라, 분석 런타임(Embedded Analytics, SAP Analytics Cloud Live Connection, Query Browser)이 뷰를 어떻게 해석할지를 결정합니다.
이 글의 목표는 다음과 같습니다.
- DIMENSION / FACT / CUBE의 역할과 차이를 한 줄로 설명할 수 있다
- 각 카테고리에 맞는 CDS View Entity 예제를 직접 작성할 수 있다
@ObjectModel.representativeKey,@DefaultAggregation등 함께 쓰이는 어노테이션을 이해한다- Cube → Query View로 연결되는 Analytical Stack 구조를 그릴 수 있다
- 흔히 발생하는 활성화 오류와 의미상 오류를 사전에 회피한다
이 글을 보기 전에
이 글은 ABAP CDS View Entity(DEFINE VIEW ENTITY) 문법과 어노테이션 개념을 알고 있다고 전제합니다. 또한 데이터 웨어하우스의 스타 스키마(Star Schema), 즉 팩트 테이블과 차원 테이블의 구분을 알면 학습 곡선이 짧아집니다. SAP S/4HANA의 Embedded Analytics 또는 SAP BTP ABAP Environment에서 분석 뷰를 노출해 본 경험이 있으면 좋습니다.
환경 / 버전 / 준비물
이 글의 예제는 다음 환경에서 검증된 패턴을 일반화한 것입니다.
- SAP S/4HANA 2022 이상 또는 SAP BTP ABAP Environment(Steampunk) 최신 릴리스
- ABAP Development Tools(ADT) for Eclipse 3.36 이상
- CDS View Entity(
DEFINE VIEW ENTITY) 문법 권장 — 레거시DEFINE VIEW(DDIC 기반)도 동일한 어노테이션을 지원하지만 신규 개발은 View Entity를 일반적으로 권장합니다 - Embedded Analytics 라이선스 또는 SAP Analytics Cloud(SAC) Live Connection
- Query Browser(SAP Fiori 앱) 또는 RSRT/RSRTS_ODP_DIS와 같은 분석 미리보기 도구
패키지는 분석 객체용으로 분리하는 것이 일반적으로 권장됩니다. 예: Z_ANALYTICS_DEMO 패키지에 인터페이스 뷰, 큐브 뷰, 쿼리 뷰를 폴더 구조로 정리합니다.
핵심 개념
분석 CDS는 스타 스키마의 ABAP 표현이라고 보면 이해가 쉽습니다. 별의 중심에 사실(Fact)이 있고, 별의 끝점마다 차원(Dimension)이 붙으며, 두 가지를 통합한 분석 모델이 큐브(Cube)입니다.
세 가지 dataCategory 비교
| 구분 | #DIMENSION | #FACT | #CUBE |
|---|---|---|---|
| 역할 | 마스터/속성 제공 | 거래 측정값 원천 | 분석 모델(스타) |
| 예시 | Customer, Product, Country, Plant | SalesOrderItem, BillingItem, GLLineItem | SalesCube, RevenueCube |
| 키 특성 | 대표 키 1개 명시 | 트랜잭션 라인 키 | FACT 키 상속 + Dim 연결 |
| 집계 | 없음(텍스트/속성) | 원본 측정값 | DefaultAggregation 선언 |
| JOIN 대상 | 다른 Dimension(계층) | 거의 없음 | FACT + 다수 DIMENSION |
비유: 마트 진열대
FACT는 영수증 라인입니다. "고객 A가 상품 X를 3개 샀다"는 raw 트랜잭션을 그대로 들고 있습니다. DIMENSION은 진열대 라벨입니다. 상품 X의 카테고리, 색상, 제조사, 단가 같은 속성을 보관합니다. CUBE는 매장 대시보드입니다. 영수증을 카테고리·기간·매장별로 자동 집계해 보여줄 수 있도록, FACT를 중심에 두고 DIMENSION을 사방으로 JOIN한 의미 모델입니다.
왜 FACT와 CUBE를 분리하는가
FACT 뷰는 "최대한 원본에 가까운 측정값"을 캡슐화합니다. 환산, 단위 보정, 회계 기간 매핑 정도만 수행합니다. CUBE는 그 위에서 차원 JOIN과 집계 의미(SUM/MIN/MAX/AVG)를 부여합니다. 이렇게 분리하면 동일한 FACT를 다른 차원 조합의 CUBE 여러 개에서 재사용할 수 있고, 차원 변경이 FACT 정의를 흔들지 않습니다.
representativeKey의 의미
@ObjectModel.representativeKey는 "이 뷰의 의미상 대표 키"를 한 개 지정합니다. DIMENSION에서 특히 중요하며, Query Browser와 SAC가 이 키를 식별자로 인식해 텍스트·계층·드릴다운을 연결합니다. 복합 키를 가진 뷰라도 대표 키는 한 개만 지정하는 것이 일반적입니다.
실전 코드 3단계
1단계 — 기본 예제: Country DIMENSION 뷰
가장 단순한 차원으로, 국가 코드와 텍스트를 노출하는 인터페이스 뷰입니다. @Analytics.dataCategory: #DIMENSION은 "이 뷰는 분석 모델에서 차원으로 쓰이는 마스터 데이터"라는 선언입니다.
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Country Dimension'
@Analytics.dataCategory: #DIMENSION
@ObjectModel.representativeKey: 'Country'
@ObjectModel.dataCategory: #DIMENSION
@VDM.viewType: #BASIC
define view entity ZI_CountryDim
as select from t005t
{
key spras as Language,
key land1 as Country,
landx as CountryName,
natio as Nationality
}
핵심은 두 가지입니다. 첫째, representativeKey로 Country를 지정해 식별자를 분명히 했습니다. 둘째, 측정값(수치)이 없고 모두 속성(텍스트)입니다. DIMENSION 뷰는 SUM 대상이 아니라 GROUP BY 대상입니다.
2단계 — 실무 시나리오: FACT 뷰와 CUBE 뷰
판매 트랜잭션을 다루는 FACT 뷰와, 이를 DIMENSION과 결합한 CUBE 뷰를 함께 구성합니다. 실무에서는 단위 변환, 통화 변환, 회계 기간 산출 등이 FACT 단계에서 자주 추가됩니다.
먼저 FACT 뷰입니다. 트랜잭션 라인 단위 측정값을 노출하고, 차원 JOIN은 의도적으로 하지 않습니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Item Fact'
@Analytics.dataCategory: #FACT
@VDM.viewType: #BASIC
@ObjectModel.representativeKey: 'SalesOrderItem'
define view entity ZI_SalesItemFact
as select from vbap
{
key vbeln as SalesOrder,
key posnr as SalesOrderItem,
matnr as Product,
kunnr as SoldToParty,
werks as Plant,
vkorg as SalesOrganization,
@Semantics.quantity.unitOfMeasure: 'OrderQuantityUnit'
kwmeng as OrderQuantity,
meins as OrderQuantityUnit,
@Semantics.amount.currencyCode: 'Currency'
netwr as NetAmount,
waerk as Currency
}
FACT에는 @DefaultAggregation을 굳이 달지 않는 경우가 많습니다. 측정값 자체는 라인 원본이며, 어떤 식으로 합산할지는 상위 CUBE가 결정하도록 둡니다.
이어서 CUBE 뷰입니다. FACT를 중심으로 여러 DIMENSION을 LEFT OUTER TO ONE JOIN으로 묶고, 측정값에 @DefaultAggregation을 부여합니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Cube'
@Analytics.dataCategory: #CUBE
@VDM.viewType: #COMPOSITE
@ObjectModel.representativeKey: 'SalesOrderItem'
define view entity ZI_SalesCube
as select from ZI_SalesItemFact as Fact
association [0..1] to ZI_CountryDim as _Country
on _Country.Country = Fact.SoldToParty // 예시 — 실제는 BP-Country 매핑
and _Country.Language = $session.system_language
association [0..1] to I_Product as _Product
on _Product.Product = Fact.Product
{
key Fact.SalesOrder,
key Fact.SalesOrderItem,
@ObjectModel.foreignKey.association: '_Country'
Fact.SoldToParty,
@ObjectModel.foreignKey.association: '_Product'
Fact.Product,
Fact.Plant,
Fact.SalesOrganization,
@DefaultAggregation: #SUM
@Semantics.quantity.unitOfMeasure: 'OrderQuantityUnit'
Fact.OrderQuantity,
Fact.OrderQuantityUnit,
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'Currency'
Fact.NetAmount,
Fact.Currency,
_Country,
_Product
}
여기서 @DefaultAggregation: #SUM이 핵심입니다. 분석 런타임이 "이 필드는 합산 가능한 측정값"임을 인지하고, 드릴다운에 따라 자동으로 SUM을 계산합니다. 통화/단위 필드는 측정값에 의미적으로 묶여 자동 분기됩니다.
3단계 — 프로덕션 고려사항
프로덕션 큐브는 성능과 권한, 단위 일관성을 동시에 고려해야 합니다. 다음 패턴을 결합한 예시입니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Cube (Hardened)'
@Analytics: { dataCategory: #CUBE, dataExtraction.enabled: true }
@VDM.viewType: #COMPOSITE
@ObjectModel.usageType: { serviceQuality: #D,
sizeCategory: #XXL,
dataClass: #TRANSACTIONAL }
@ObjectModel.representativeKey: 'SalesOrderItem'
define view entity ZI_SalesCube_P
as select from ZI_SalesItemFact as Fact
association [0..1] to ZI_CountryDim as _Country
on _Country.Country = Fact.SoldToParty
and _Country.Language = $session.system_language
{
key Fact.SalesOrder,
key Fact.SalesOrderItem,
Fact.Plant,
Fact.SalesOrganization,
@ObjectModel.foreignKey.association: '_Country'
Fact.SoldToParty,
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'TargetCurrency'
@AnalyticsDetails.exceptionAggregationSteps: [ { exceptionAggregationBehavior: #LAST,
exceptionAggregationElements: [ 'SalesOrderItem' ] } ]
currency_conversion(
amount => Fact.NetAmount,
source_currency => Fact.Currency,
target_currency => cast('USD' as abap.cuky),
exchange_rate_date => $session.system_date,
error_handling => 'SET_TO_NULL' ) as NetAmountUSD,
cast('USD' as abap.cuky) as TargetCurrency,
_Country
}
핵심 포인트는 다음과 같습니다.
dataExtraction.enabled: true로 SAC/BW Extraction 호환성을 열어둡니다usageType으로 서비스 품질, 크기, 데이터 클래스를 명시해 정적 검사기가 권장 패턴을 유지하도록 합니다currency_conversion빌트인으로 통화 환산을 FACT 레벨이 아닌 CUBE 레벨에서 표현해, FACT 재사용성을 유지합니다exceptionAggregationSteps로 특정 차원에서는 SUM이 아닌 LAST 등을 적용 — 재고/잔액성 측정값에 일반적으로 권장됩니다- 테스트는 ABAP Unit + CDS Test Double Framework로 FACT를 모킹하고, CUBE 출력의 합계가 기대값과 일치하는지 확인합니다
흔한 실수 / 트러블슈팅
Q1. CUBE인데 DefaultAggregation을 빠뜨려서 측정값이 안 잡힙니다
증상: Query Browser 또는 SAC에서 필드가 "Dimension"으로만 인식되어 SUM이 안 됩니다. 원인: @DefaultAggregation이 없으면 분석 런타임은 해당 필드를 측정값으로 분류하지 않습니다. 해결: 수치 필드마다 #SUM, #MIN, #MAX, #AVG 중 의미에 맞는 값을 명시하세요. 통화·단위 필드는 측정값이 아니라 속성으로 두고 @Semantics.amount.currencyCode 등으로 연결합니다.
Q2. DIMENSION 활성화 시 "representativeKey가 키 필드가 아니다" 오류
증상: ZI_CountryDim 활성화 중 representativeKey로 지정한 필드가 키 필드가 아니라는 메시지가 발생합니다. 원인: @ObjectModel.representativeKey는 반드시 뷰의 키 필드 중 하나를 가리켜야 합니다. 해결: key 키워드가 붙은 필드 이름과 정확히 일치하는지 확인하세요. 한 뷰에 대표 키는 한 개만 둡니다.
Q3. CUBE에서 INNER JOIN을 썼더니 매출이 사라집니다
증상: 차원이 비어 있는 트랜잭션이 CUBE 결과에서 누락됩니다. 원인: 차원과 FACT 사이를 INNER JOIN으로 묶으면 차원에 매칭이 없는 FACT 라인이 제거됩니다. 해결: 차원 연결은 association [0..1] to ... 또는 LEFT OUTER TO ONE JOIN으로 정의해 FACT의 보존성을 지키세요. 분석 모델의 측정값 합계는 "FACT 전수"가 기준이 되어야 합니다.
Q4. FACT 뷰에 직접 차원 JOIN을 넣고 싶을 때
가능하긴 하지만 권장되지 않습니다. FACT는 원본에 가까울수록 다양한 CUBE에서 재사용하기 쉽고, JOIN을 FACT에 박아 두면 다른 차원 조합으로 분석하고 싶을 때 새 FACT를 만들어야 합니다. 차원 결합은 CUBE 레이어로 미루는 것이 일반적으로 권장됩니다.
Q5. Query Browser에 뷰가 안 보입니다
원인 후보: dataCategory가 CUBE/QUERY가 아니거나, @VDM.viewType이 누락됐거나, OData/Analytical 서비스가 발행되지 않은 경우입니다. CUBE 자체로 조회는 가능하지만 보통 그 위에 @Analytics.query: true인 Query 뷰를 만들고 그것을 검색합니다.
다음 단계 / 관련 주제
CUBE까지 만들었다면, 그 위에 사용자가 직접 조회할 Query View를 작성하는 것이 자연스러운 다음 단계입니다. Query 뷰는 @Analytics.query: true로 선언하며, CUBE에서 가져올 필드, 기본 행/열 배치(@AnalyticsDetails.query.axis), 필터(@Consumption.filter), 변수(@AnalyticsDetails.query.variableSequence)를 정의합니다. Query 뷰는 SAC, Query Browser, RSRT에서 곧바로 실행할 수 있는 분석 인공물입니다.
이후에는 KPI/Calculated Measure 어노테이션, Hierarchy(@Hierarchy.parentChild), Restricted Measure, Input Parameter, 그리고 SAP BTP ABAP Environment의 Analytical OData(Service Binding의 Analytical Type) 발행으로 학습을 확장하면 좋습니다.
댓글 0
아직 댓글이 없습니다.