이 글이 답하는 질문
- DefaultAggregation 어노테이션은 왜 필요한가?
- SUM, MAX, MIN, COUNT, NONE 각각 언제 쓰는가?
- 통화 코드나 Dimension 필드에 SUM을 붙이면 어떤 오류가 생기는가?
- Query Browser에서 집계 오류를 어떻게 확인하는가?
이 글은 SAP S/4HANA 2023 ABAP for Cloud Development 환경에서 Analytical Query를 설계할 때, CUBE View의 측정값(Measure)에 어떤 집계 방식을 선언해야 하는지를 다룹니다. 글을 읽고 나면 SUM과 NONE을 구분해서 쓸 수 있고, 잘못된 집계 선언이 Query Browser에서 어떻게 드러나는지 진단할 수 있게 됩니다.
체크리스트로 정리하면 다음과 같습니다.
- 측정 가능한 숫자 필드와 단순 분류 코드를 구별할 수 있다
- 금액·수량 컬럼에
@DefaultAggregation: #SUM을 정확히 부여할 수 있다 - 통화 코드, 단위, 상태 코드 같은 Dimension 후보 필드에
#NONE을 선언하는 이유를 설명할 수 있다 - Query Browser 실행 시 집계 오류가 어떤 형태로 나타나는지 알아챌 수 있다
이 글을 보기 전에
다음 세 가지 개념은 이 글의 모든 코드를 이해하는 데 기본 전제가 됩니다.
- ABAP CDS View 기본 문법 —
define view entity키워드,select from절, association 정의, 어노테이션 표기 규칙 - @Analytics.dataCategory #CUBE 개념 — Cube View가 Fact 데이터를 담는 분석 모델이며, Dimension View와 association으로 연결된다는 점
- Analytical Query 3층 구조 — Interface View(I_*)에서 데이터 정제, Cube View(C_*)에서 집계 선언, Query View(C_*Query)에서 화면 표현을 분리하는 구조
위 세 가지가 익숙하지 않다면 AQ 기초 1편(CDS Interface), 2편(@Analytics.dataCategory) 시리즈를 먼저 읽고 오는 것을 권장합니다.
환경 / 버전 / 준비물
이 글의 모든 코드는 다음 환경에서 검증되었습니다.
- SAP S/4HANA 2023, On-Premise 또는 Private Cloud Edition
- ABAP for Cloud Development (Steampunk 호환 모드)
- ABAP Development Tools (ADT) for Eclipse 2024-03 이상
- SAP Fiori Launchpad의 Query Browser (앱 ID:
F1538) - 샘플 테이블:
I_SalesOrderItem(표준 릴리즈 CDS), 또는 자체 ZTAB 사용 가능
실습을 따라가려면 ADT에서 $TMP 또는 사용자 패키지에 CDS View를 생성할 권한이 필요하며, Query Browser 접근 권한(S_RS_AUTH 또는 카탈로그 SAP_BR_ANALYTICS_SPECIALIST)이 있어야 결과를 확인할 수 있습니다.
DefaultAggregation이란 무엇인가 — CUBE View의 집계 선언
@DefaultAggregation은 CDS View의 각 필드가 "측정값(Measure)인가, 분류값(Dimension)인가"를 선언하는 어노테이션입니다. Analytical Engine이 Query를 실행할 때, 이 어노테이션을 기준으로 GROUP BY를 자동 생성하기 때문에 잘못 붙이면 결과가 통째로 어긋납니다.
비유하자면 엑셀 피벗 테이블을 생각하면 쉽습니다. 피벗을 만들 때 "값" 영역에 끌어다 놓을 수 있는 필드는 숫자 합계, 평균, 최대 같은 수치 집계가 가능한 컬럼이고, "행/열" 영역에 끌어다 놓는 필드는 그 자체로 카테고리가 되는 코드·이름입니다. @DefaultAggregation은 엑셀이 자동으로 판정하던 그 구분을 CDS 모델에 명시적으로 적어두는 역할을 합니다.
핵심 값은 다섯 가지입니다.
#SUM— 합산. 금액, 수량 등 누적이 의미 있는 수치#MAX— 최댓값. 그룹 내 가장 큰 값을 가져옴#MIN— 최솟값. 그룹 내 가장 작은 값을 가져옴#COUNT— 건수 집계. 그룹 내 행 개수를 셈#NONE— 집계하지 않음. Dimension 후보 또는 단위·통화 코드처럼 합산이 무의미한 필드
도식으로 표현하면 다음과 같습니다.
+--------------------------------------------+
| CUBE View Field |
+--------------------------------------------+
| Numeric Measure ----> #SUM / #MAX / #MIN |
| Row Count Field ----> #COUNT |
| Currency / UoM ----> #NONE |
| Key Code (Dim) ----> #NONE 또는 미선언 |
+--------------------------------------------+
여기서 가장 자주 헷갈리는 부분이 통화 코드(CurrencyCode)와 단위 코드(UnitOfMeasure)입니다. 두 필드는 숫자처럼 보이지 않지만 금액·수량과 항상 함께 다녀야 하기 때문에 Measure 가까이에 선언됩니다. 그러나 절대 #SUM을 붙여서는 안 되며, #NONE으로 명시하거나 아무것도 붙이지 않아 Dimension으로 남겨야 합니다.
SUM — 금액·수량 합산: 가장 많이 쓰는 집계 방식
가장 흔한 케이스는 매출 금액, 주문 수량, 재고량 같은 컬럼입니다. 이런 필드는 Query Browser에서 사용자가 "월별 매출 합계", "지역별 주문량 합계"를 보고 싶을 때마다 자동으로 합산되어야 합니다. #SUM은 Analytical Engine에게 "이 필드는 합쳐도 의미가 유지된다"라고 알려주는 신호입니다.
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Cube - SUM example'
@Analytics.dataCategory: #CUBE
define view entity ZC_SALES_CUBE_STEP1
as select from I_SalesOrderItem
{
key SalesOrder,
key SalesOrderItem,
Product,
SoldToParty,
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #SUM
NetAmount,
@Semantics.quantity.unitOfMeasure: 'OrderQuantityUnit'
@DefaultAggregation: #SUM
RequestedQuantity,
@Semantics.currencyCode: true
@DefaultAggregation: #NONE
TransactionCurrency,
@Semantics.unitOfMeasure: true
@DefaultAggregation: #NONE
OrderQuantityUnit
}
위 예시에서 NetAmount와 RequestedQuantity 두 필드만 합산 대상이며, 통화·단위 코드는 #NONE으로 명시했습니다. Query를 실행하면 결과적으로 SELECT SUM(NetAmount), SUM(RequestedQuantity) ... GROUP BY Product, SoldToParty, TransactionCurrency, OrderQuantityUnit 형태로 생성됩니다.
MAX·MIN — 최고가·최저가: 극값 집계
합산이 의미 없지만 그룹 내 극단값이 의미 있는 경우가 있습니다. 예를 들어 "고객별 최고 단가", "지점별 최저 재고일자" 같은 시나리오입니다. 이때는 #MAX 또는 #MIN을 사용합니다.
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Cube - MAX/MIN example'
@Analytics.dataCategory: #CUBE
define view entity ZC_SALES_CUBE_STEP2
as select from I_SalesOrderItem
{
key SalesOrder,
key SalesOrderItem,
Product,
SoldToParty,
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #SUM
NetAmount,
// 단가는 합산하면 의미가 깨짐 — 최댓값 집계
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #MAX
NetPriceAmount as MaxUnitPrice,
// 같은 데이터를 최솟값으로 한 번 더 노출
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #MIN
NetPriceAmount as MinUnitPrice,
@Semantics.currencyCode: true
@DefaultAggregation: #NONE
TransactionCurrency
}
여기서 핵심은 같은 원본 컬럼(NetPriceAmount)을 별칭(alias)으로 두 번 노출해 각각 다른 집계를 부여한 점입니다. Query Browser에서는 MaxUnitPrice와 MinUnitPrice가 별개 측정값으로 나타나며, 사용자가 "Product별 최대 단가와 최소 단가"를 한 화면에서 비교할 수 있습니다. 단가 자체에 #SUM을 붙이지 않은 점에 유의하세요. 단가는 합계가 비즈니스적으로 의미 없는 대표적 필드입니다.
COUNT·NONE — 건수 vs 집계 안 함
#COUNT는 "이 필드가 NULL이 아닌 행의 개수"를 세는 집계입니다. 주문 건수, 고객 수, 활성 계약 수처럼 "몇 개인가"를 알고 싶을 때 사용합니다.
// SalesOrder 건수를 세는 Measure
@DefaultAggregation: #COUNT
SalesOrder as OrderCount,
// 동일 필드를 Key로도 노출하려면 별도 별칭으로
key SalesOrder
반면 #NONE은 "이 필드는 집계하지 말고 Dimension처럼 GROUP BY 절에 포함시켜라"는 의미입니다. 통화 코드, 단위, 상태 코드, 회사 코드, 플랜트 코드 등이 여기에 해당합니다.
실무 팁: Key 필드에는
@DefaultAggregation을 붙이지 않는 것이 일반적입니다. Key는 정의상 그룹화 기준이 되기 때문입니다.#NONE은 Key가 아니지만 합산 의미가 없는 코드 필드에 명시적으로 붙입니다.
흔한 실수: 통화 코드에 SUM을 붙이면 생기는 Query Browser 오류
가장 자주 발생하는 실수는 다음 세 가지입니다.
- 실수 1 —
TransactionCurrency같은 통화 코드 필드에#SUM을 붙임. 활성화는 통과되지만 Query Browser 실행 시"Aggregation behavior is inconsistent"류의 메시지가 나오거나, 통화 코드가 문자처럼 합쳐져 (예: "USDUSDEUR") 결과가 깨짐 - 실수 2 — 단가 필드에
#SUM을 붙임. 활성화도 통과되고 실행도 되지만, 의미적으로 단가의 합은 무의미하므로 비즈니스 사용자가 잘못된 KPI를 받게 됨 - 실수 3 — 모든 숫자 필드에 동일하게
#SUM을 일괄 적용. 비율 필드(%), 평균 점수 같은 비가산(non-additive) 측정값까지 합산되어 KPI가 부풀려짐
FAQ
Q1. #SUM을 통화 필드에 붙였는데 활성화가 성공했습니다. 진짜 문제가 있는 건가요?
활성화는 구문 검증 단계라 통과될 수 있습니다. 그러나 Query Browser에서 해당 Query를 실행해 통화 코드를 행에 끌어다 놓는 순간 Analytical Engine이 통화 코드 컬럼을 측정값으로 해석해 GROUP BY에서 제외하고, 결과적으로 환율이 다른 금액이 한 셀에 섞여 표시됩니다. @Semantics.amount.currencyCode 참조 무결성 체크도 깨질 수 있습니다.
Q2. Key 필드에는 어떤 어노테이션을 붙여야 하나요?
Key는 본질적으로 Dimension이므로 @DefaultAggregation을 생략하는 것이 일반적인 패턴입니다. 명시적으로 표현하고 싶다면 #NONE을 붙여도 무방하지만, 가독성 면에서 생략을 권장합니다.
Q3. Query Browser에서 집계 오류를 어떻게 진단하나요?
Fiori Launchpad의 Query Browser(앱 ID F1538)에서 해당 Query View를 검색해 미리보기를 실행합니다. 측정값 영역에 있어야 할 필드가 행 영역에 나타나거나, 행 영역에 있어야 할 코드가 합산된 듯한 큰 숫자로 표시되면 @DefaultAggregation 선언을 의심합니다. ADT의 Data Preview(F8)에서는 단순 SELECT만 확인되므로 집계 오류는 드러나지 않습니다.
완성 코드 예시: SalesCube CUBE View
지금까지의 규칙을 종합한 프로덕션 수준의 SalesCube 예시입니다. Interface View가 이미 존재한다고 가정하며, 이 Cube 위에 별도 Query View를 올려 Fiori에 노출하는 구조입니다.
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Cube with full aggregation set'
@Metadata.allowExtensions: true
@Analytics.dataCategory: #CUBE
@Analytics.internalName: #LOCAL
@VDM.viewType: #COMPOSITE
@ObjectModel.usageType.serviceQuality: #X
@ObjectModel.usageType.sizeCategory: #L
@ObjectModel.usageType.dataClass: #MIXED
define view entity ZC_SALES_CUBE
as select from I_SalesOrderItem as Item
association [0..1] to I_Product as _Product
on $projection.Product = _Product.Product
association [0..1] to I_Customer as _Customer
on $projection.SoldToParty = _Customer.Customer
association [0..1] to I_CalendarDate as _CreationDate
on $projection.CreationDate = _CreationDate.CalendarDate
{
// ---------- Keys (Dimension) ----------
key Item.SalesOrder,
key Item.SalesOrderItem,
// ---------- Dimension 후보 ----------
@ObjectModel.foreignKey.association: '_Product'
Item.Product,
@ObjectModel.foreignKey.association: '_Customer'
Item.SoldToParty,
@ObjectModel.foreignKey.association: '_CreationDate'
Item.CreationDate,
// ---------- Measures: 합산 ----------
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #SUM
@EndUserText.label: 'Net Amount (Sum)'
Item.NetAmount,
@Semantics.quantity.unitOfMeasure: 'OrderQuantityUnit'
@DefaultAggregation: #SUM
@EndUserText.label: 'Order Quantity (Sum)'
Item.RequestedQuantity,
// ---------- Measures: 극값 ----------
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #MAX
@EndUserText.label: 'Max Unit Price'
Item.NetPriceAmount as MaxUnitPrice,
@Semantics.amount.currencyCode: 'TransactionCurrency'
@DefaultAggregation: #MIN
@EndUserText.label: 'Min Unit Price'
Item.NetPriceAmount as MinUnitPrice,
// ---------- Measures: 건수 ----------
@DefaultAggregation: #COUNT
@EndUserText.label: 'Order Item Count'
Item.SalesOrderItem as ItemCount,
// ---------- Currency / UoM (집계 안 함) ----------
@Semantics.currencyCode: true
@DefaultAggregation: #NONE
Item.TransactionCurrency,
@Semantics.unitOfMeasure: true
@DefaultAggregation: #NONE
Item.OrderQuantityUnit,
// ---------- Associations ----------
_Product,
_Customer,
_CreationDate
}
이 Cube를 활성화한 뒤, 위에 @Analytics.query: true를 가진 Query View를 하나 더 만들어 Fiori Query Browser에 노출하면 다음과 같은 분석 시나리오가 가능해집니다.
- 고객별 / 제품별 NetAmount 합계 비교
- 제품별 단가 분포(최대·최소)와 평균 가격 추론
- 월별 주문 라인 건수(ItemCount) 추세 분석
- 통화별로 분리된 매출 합계 (TransactionCurrency가 자동으로 GROUP BY에 포함)
다음 단계 / 관련 주제
이 글에서 다룬 @DefaultAggregation은 Cube View의 골격을 잡는 어노테이션이지만, 실제 화면 표현은 Query View 단계에서 결정됩니다. 다음 주제로 이어가면 좋습니다.
- AQ 기초 4편 —
@AnalyticsDetails.query.axis로 Row/Column 기본 배치 지정 - AQ 기초 5편 —
@AnalyticsDetails.query.formula로 측정값 간 계산식 추가 (예: 평균 단가 = 합계금액 / 합계수량) - 심화 — Non-cumulative Measure, Exception Aggregation(
@DefaultAggregation: #FORMULA) 활용 - 운영 — Query Browser와 SAC(SAP Analytics Cloud) Live Connection 연동 시 집계 동작 차이
참고 자료
- help.sap.com — CDS Annotations for Analytical Modeling
- help.sap.com — Defining Analytical Queries with CDS
- help.sap.com — DefaultAggregation Annotation Reference
- SAP Community — Analytical Query 태그 블로그 모음
- SAP Community — ABAP CDS Q&A
- help.sap.com — Fiori Apps Library (Query Browser F1538 검색)
이 글의 모든 코드는 SAP S/4HANA 2023 환경 기준이며, 다른 릴리즈에서는 어노테이션 지원 범위가 다를 수 있으니 실제 시스템에서 ADT의 코드 어시스트로 사용 가능한 값을 다시 확인하는 것을 권장합니다.
댓글 0
아직 댓글이 없습니다.