개요 및 이번 글에서 다룰 내용
ABAP Analytical Query는 SAP S/4HANA 및 BTP ABAP Environment에서 분석 보고서를 정의하는 핵심 수단입니다. 그중에서도 Calculated Measure는 기존 측정값들을 조합하여 새로운 지표를 즉석에서 계산하는 기능으로, 매출 성장률, 마진율, 평균 단가와 같은 파생 지표를 데이터베이스 컬럼 없이 정의할 수 있습니다. 이 글에서는 @AnalyticsDetails.query.formula 어노테이션을 활용하여 Calculated Measure를 정의하는 방법, NDIV0()로 0 나누기 오류를 방지하는 패턴, 그리고 Restricted Measure와 결합하여 기간 비교 KPI를 만드는 실전 시나리오를 다룹니다.
- Calculated Measure의 정의와 동작 위치(런타임 계산)
- formula 표현식 문법 및 사용 가능 함수
- 0으로 나누기 방어, 단위/통화 변환 주의점
- Restricted Measure와 조합한 성장률(YoY, MoM) 패턴
- 프로덕션 환경에서의 성능, 테스트, 권한 고려사항
알아두면 좋은 배경 지식
이 글의 내용을 원활히 따라가려면 CDS View 작성 경험, 특히 @Analytics.dataCategory가 #CUBE/#DIMENSION인 Analytical View 구조와 Analytical Query View의 차이를 이해하고 있어야 합니다. 또한 측정값(measure)과 차원(dimension)의 구분, Aggregation(SUM/MIN/MAX/AVG) 동작 방식, 그리고 ABAP CDS 어노테이션 문법에 익숙한 것을 권장합니다. BW의 Calculated Key Figure를 다뤄본 경험이 있다면 개념적으로 유사하므로 적응이 쉽습니다.
환경 및 준비 사항
실습 환경 기준은 다음과 같이 잡는 것을 권장합니다. 사용 가능한 어노테이션 및 함수가 릴리즈에 따라 일부 차이가 있으므로 SAP Note 및 help.sap.com의 ABAP CDS 레퍼런스에서 자신의 릴리즈에 맞는 버전을 확인하는 것이 안전합니다.
- SAP S/4HANA 2022 FPS02 이상 또는 SAP BTP ABAP Environment(Steampunk) 2308 이상
- ABAP Development Tools(ADT) for Eclipse 최신 버전
- SAP HANA 2.0 SPS06+ 또는 BTP ABAP Environment 기본 HANA
- 분석 결과 검증용 Fiori Elements Analytical List Page(ALP) 또는 RSRT(S/4HANA on-prem)
- 샘플 데이터:
I_SalesOrderItem,I_Product등 SAP 제공 Released CDS View
Calculated Measure는 데이터베이스에 컬럼을 추가하지 않고 쿼리 실행 시 계산되므로, 별도 Activation 외에 인프라 준비물은 적지만 결과 검증을 위해 ALP 앱 또는 Query Browser 도구를 권장합니다.
핵심 개념: Calculated Measure는 어떻게 동작하는가
Calculated Measure는 Analytical Query View(@Analytics.query: true)의 측정값 필드에 @AnalyticsDetails.query.formula 어노테이션으로 표현식을 부여한 가상 측정값입니다. 데이터베이스 테이블에는 존재하지 않으며, 사용자가 해당 측정값을 선택한 순간 분석 엔진(Analytic Manager)이 다른 measure 값을 가져와 행 단위가 아닌 집계 이후 단계에서 계산합니다.
비유하자면, 영수증에 적힌 개별 금액(원천 measure)을 모두 더한 합계 뒤에 "합계의 10%를 부가세로" 표기하는 것과 같습니다. 영수증 한 줄마다 부가세를 계산하지 않고, 마지막에 한 번 계산하기 때문에 그룹핑 수준(연/월/제품/지역)에 따라 결과가 다르게 보입니다.
핵심 동작 규칙은 다음과 같습니다.
- 집계 후 계산(post-aggregation): SUM 같은 집계가 먼저 일어난 뒤 formula가 평가됩니다. 따라서 "가격 × 수량의 합"과 "가격의 합 × 수량의 합"은 결과가 다릅니다.
- Drill 상태 의존성: 사용자가 어느 차원으로 분해(drill-down)했는지에 따라 분모/분자가 달라져 결과가 동적으로 바뀝니다.
- 단위/통화: formula에서 단위가 다른 measure를 단순 사칙연산하면 의도치 않은 결과가 나올 수 있어
@Semantics.amount.currencyCode등으로 명시해야 합니다. - 안전한 나눗셈: 분모가 0일 가능성이 있으면
NDIV0()을 감싸 NaN/오류를 방지합니다.
도식적으로 보면 다음과 같은 흐름입니다.
[원천 CDS Cube]
| (SUM/COUNT 등 집계)
v
[집계된 Measure 집합]
| (formula 평가)
v
[Calculated Measure 값]
|
v
[Fiori / Excel / RSRT 표시]
Restricted Measure는 "특정 조건을 만족하는 행의 합계"를 의미하는 별도 패턴이며, Calculated Measure와 조합하면 "올해 매출 / 작년 매출 - 1" 같은 비교 KPI를 쉽게 만들 수 있습니다.
실전 코드: 3단계로 만드는 Calculated Measure
1단계 — 기본 Analytical Query에 단순 Calculated Measure 추가
가장 단순한 형태로 평균 단가(Average Unit Price)를 만들어 봅니다. 분모로 사용하는 수량이 0일 수 있어 NDIV0()으로 감쌉니다.
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Query - Basic Calculated Measure'
@Analytics.query: true
@OData.publish: true
define view entity ZQ_SALES_BASIC
as select from I_SalesOrderItem
{
key SalesOrderItem,
Product,
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'TransactionCurrency'
NetAmount,
@DefaultAggregation: #SUM
@Semantics.quantity.unitOfMeasure: 'RequestedQuantityUnit'
RequestedQuantity,
TransactionCurrency,
RequestedQuantityUnit,
@DefaultAggregation: #FORMULA
@EndUserText.label: 'Avg Unit Price'
@Semantics.amount.currencyCode: 'TransactionCurrency'
@AnalyticsDetails.query.formula: 'NDIV0( NetAmount / RequestedQuantity )'
cast( 0 as abap.dec( 23, 2 ) ) as AvgUnitPrice
}
포인트는 세 가지입니다. (1) Calculated Measure도 select list에 컬럼으로 선언하되 더미 값(cast(0 ...))을 줍니다. (2) @DefaultAggregation: #FORMULA로 표시해야 분석 엔진이 formula로 인식합니다. (3) formula 안의 식별자는 같은 view의 다른 필드 이름을 그대로 참조합니다.
2단계 — 실무 시나리오: 마진율과 Restricted Measure 조합
실제 보고서에서는 "매출 - 원가"를 매출로 나눈 마진율(Margin %), 그리고 특정 기간(예: 올해와 작년)의 매출을 비교하는 성장률(YoY Growth) 같은 KPI가 필요합니다. Restricted Measure로 기간별 매출을 잘라낸 뒤, Calculated Measure로 비교하는 패턴을 사용합니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Query - Margin & YoY Growth'
@Analytics.query: true
define view entity ZQ_SALES_KPI
as select from ZI_SalesCube -- 사전 정의된 CUBE view
{
@AnalyticsDetails.query.axis: #ROWS
Product,
CalendarYear,
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'DisplayCurrency'
NetRevenue,
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'DisplayCurrency'
CostOfGoodsSold,
// Restricted Measure: 올해 매출
@DefaultAggregation: #SUM
@AnalyticsDetails.query.variableSequence: 10
@AnalyticsDetails.query.restriction.singleRestriction: { selectionElement: 'CalendarYear',
range: { sign: #I, option: #EQ, low: '$$CurrentYear$$' } }
@Semantics.amount.currencyCode: 'DisplayCurrency'
NetRevenue as RevenueCurrentYear,
// Restricted Measure: 작년 매출
@DefaultAggregation: #SUM
@AnalyticsDetails.query.restriction.singleRestriction: { selectionElement: 'CalendarYear',
range: { sign: #I, option: #EQ, low: '$$PreviousYear$$' } }
@Semantics.amount.currencyCode: 'DisplayCurrency'
NetRevenue as RevenuePreviousYear,
// Calculated Measure: 마진율 (%)
@DefaultAggregation: #FORMULA
@EndUserText.label: 'Margin %'
@Semantics.quantity.unitOfMeasure: 'PercentUnit'
@AnalyticsDetails.query.formula: 'NDIV0( ( NetRevenue - CostOfGoodsSold ) / NetRevenue ) * 100'
cast( 0 as abap.dec( 9, 2 ) ) as MarginPercent,
// Calculated Measure: YoY 성장률 (%)
@DefaultAggregation: #FORMULA
@EndUserText.label: 'YoY Growth %'
@AnalyticsDetails.query.formula: 'NDIV0( ( RevenueCurrentYear - RevenuePreviousYear ) / RevenuePreviousYear ) * 100'
cast( 0 as abap.dec( 9, 2 ) ) as YoYGrowthPercent
}
여기서 핵심은 (1) Restricted Measure RevenueCurrentYear, RevenuePreviousYear가 같은 NetRevenue를 서로 다른 연도 필터로 잘라낸 가상 컬럼이라는 점, (2) Calculated Measure가 이 두 측정값을 단순히 사칙연산으로 참조한다는 점입니다. 결과적으로 사용자는 한 화면에서 "올해 매출, 작년 매출, 성장률, 마진율"을 한꺼번에 볼 수 있습니다.
System Variable $$CurrentYear$$, $$PreviousYear$$는 릴리즈에 따라 명칭이 다를 수 있으니, 환경에 맞춰 사용 가능한 시스템 변수 또는 입력 파라미터로 대체하는 것이 안전합니다.
3단계 — 프로덕션: 단위 일관성, 권한, 성능, 테스트
실제 배포 시에는 단위/통화 일관성, 권한, 성능 모니터링까지 함께 챙겨야 합니다. 다음은 통화 변환과 입력 파라미터를 더한 형태입니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales KPI Query - Production'
@Analytics.query: true
@Metadata.allowExtensions: true
define view entity ZQ_SALES_KPI_PROD
with parameters
P_DisplayCurrency : vdm_v_display_currency,
P_ExchangeRateType: vdm_v_exchange_rate_type,
P_KeyDate : vdm_v_key_date
as select from ZI_SalesCube
association [0..1] to I_Product as _Product on $projection.Product = _Product.Product
{
@AnalyticsDetails.query.axis: #ROWS
Product,
CalendarYear,
CalendarMonth,
@Semantics.amount.currencyCode: 'DisplayCurrency'
@DefaultAggregation: #SUM
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_Currency', element: 'Currency' } }]
currency_conversion(
amount => NetRevenue,
source_currency => TransactionCurrency,
target_currency => $parameters.P_DisplayCurrency,
exchange_rate_type => $parameters.P_ExchangeRateType,
exchange_rate_date => $parameters.P_KeyDate,
error_handling => 'SET_TO_NULL'
) as NetRevenue,
$parameters.P_DisplayCurrency as DisplayCurrency,
@DefaultAggregation: #FORMULA
@EndUserText.label: 'Margin %'
@AnalyticsDetails.query.formula: 'NDIV0( ( NetRevenue - CostOfGoodsSold ) / NetRevenue ) * 100'
cast( 0 as abap.dec( 9, 2 ) ) as MarginPercent,
@DefaultAggregation: #FORMULA
@EndUserText.label: 'MoM Growth %'
@AnalyticsDetails.query.formula: 'NDIV0( ( RevenueThisMonth - RevenueLastMonth ) / RevenueLastMonth ) * 100'
cast( 0 as abap.dec( 9, 2 ) ) as MoMGrowthPercent
}
프로덕션 체크리스트로는 다음을 권장합니다.
- 권한:
@AccessControl.authorizationCheck: #CHECK유지, DCL(Data Control Language) 분리 - 성능: 원천 Cube에 적절한 SAP HANA 인덱스/파티셔닝, ST05/SQL Trace로 실행 계획 검증
- 테스트: ABAP Unit +
CL_SADL_GTK_TEST_FACTORY또는 RSRT에서 동일 차원 조합으로 회귀 테스트 - 관측: Application Log(
BAL_LOG_*) 또는 BTP의 Application Logging Service로 쿼리 호출 추적 - 문서화:
@EndUserText.label과@EndUserText.quickInfo로 의미를 명시하여 ALP 사용자가 오해하지 않도록 함
자주 만나는 함정과 해결법
Calculated Measure는 직관적으로 보이지만 잘못 쓰면 "숫자가 안 맞는다"는 컴플레인의 원흉이 됩니다. 자주 보는 함정을 FAQ 형식으로 정리합니다.
- Q1. 행 단위 합계는 맞는데 합계 행에서 값이 이상해요.
A. Calculated Measure는 집계 이후에 계산되기 때문입니다. 예를 들어 마진율은 행마다 (매출-원가)/매출이지만, 합계 행은 (총매출-총원가)/총매출이라 단순 평균과 다릅니다. 의도된 동작이며, 사용자에게 "가중 평균"임을 안내하는 것이 좋습니다. - Q2. NDIV0를 썼는데도 결과가 비어 보입니다.
A. 분모가 0이 아니라 NULL인 경우가 흔합니다.NDIV0은 0 처리에만 안전하므로 NULL 가능성이 있으면COALESCE(measure, 0)또는 미리cast로 0 치환을 권장합니다. - Q3. formula에서 다른 Calculated Measure를 참조해도 되나요?
A. 일반적으로 가능하지만 순환 참조가 생기지 않도록 주의해야 합니다. 의존 관계가 깊어지면 디버깅이 어려워지므로 중간 measure를 명시적으로 분리하고, 한 view 안에서 의존 단계를 2~3단계 이내로 유지하는 것을 권장합니다. - Q4. 통화/단위가 다른 measure를 더하면 어떻게 되나요?
A. 분석 엔진은 단위가 다르면 "*"(혼합 단위) 표시로 합산을 거부할 수 있습니다. formula 사용 전에currency_conversion/unit_conversion으로 정렬하는 것이 안전합니다.
그 외에 자주 보는 실수로는, @DefaultAggregation: #FORMULA를 빠뜨려 단순 컬럼으로 인식되거나, Restricted Measure의 selectionElement에 존재하지 않는 필드명을 적어 활성화 단계에서 에러가 나는 경우가 있습니다. ADT의 Problems 뷰에서 경고를 무시하지 말고 모두 처리하는 습관을 권장합니다.
다음으로 살펴보면 좋은 주제
Calculated Measure에 익숙해졌다면 다음 주제로 확장해 볼 수 있습니다. Exception Aggregation(다른 차원 기준으로 다시 집계)은 "고유 고객 수" 같은 KPI에 필수입니다. Input Parameter와 Variable을 활용해 사용자가 직접 임계값을 입력하는 인터랙티브 대시보드도 만들 수 있고, Hierarchies와 결합하면 조직 단위별 마진을 드릴다운할 수 있습니다. 마지막으로 BTP 환경이라면 SAC(SAP Analytics Cloud) Live Connection으로 동일 쿼리를 시각화하여 활용 범위를 넓히는 것이 권장됩니다.
- Exception Aggregation (
@DefaultAggregation: #COUNT_DISTINCT,#FIRST,#LAST) - Hierarchies (
@Hierarchy.parentChild)와 Calculated Measure 조합 - Input Parameter / Variable Sequence를 통한 사용자 입력
- SAC Live Connection 및 Fiori Analytical List Page 연동
참고 자료
- help.sap.com - ABAP CDS Analytical Queries 개요
- help.sap.com - @AnalyticsDetails.query.formula 어노테이션 레퍼런스
- help.sap.com - Restricted Measures in Analytical Queries
- help.sap.com - Currency Conversion in CDS
- SAP Developers - ABAP Analytical Query 튜토리얼
- SAP Community Blogs - ABAP CDS Views
핵심 한 줄
Calculated Measure는 @AnalyticsDetails.query.formula로 집계 이후 단계에서 평가되는 가상 측정값이며, NDIV0()와 Restricted Measure를 결합하면 마진율과 YoY 성장률 같은 KPI를 안전하고 재사용 가능하게 정의할 수 있습니다.
댓글 0
아직 댓글이 없습니다.