AQ 실전 4/5 — Calculated Measures: @AnalyticsDetails.query.formula 완전 정복
수정: 2026년 6월 16일
👁 0 · ♥ 0
# AQ 실전 4/5 — Calculated Measures: @AnalyticsDetails.query.formula 완전 정복
분석 쿼리 시리즈도 어느덧 네 번째 편에 도착했다. 1편에서 `@Analytics.query`로 쿼리 뷰의 골격을 잡았고, 2편에서 `axis`로 행/열을 배치했으며, 3편에서는 필터와 변수로 사용자 상호작용을 설계했다. 이번 4편은 그 위에 한 겹을 더 올린다. 바로 **계산식 측정값(Calculated Measure)** 이다. CDS View 안에서 단순히 데이터를 끌어오는 것을 넘어, **수식 자체를 메타데이터로 선언**하고 런타임에 Analytic Engine이 자동으로 계산하게 만드는 방식이다. 핵심 도구는 단 하나, `@AnalyticsDetails.query.formula` 어노테이션이다.
---
## 1. 왜 CDS View에서 계산식 측정값이 필요한가
분석 현장에서 가장 자주 마주치는 요구사항은 의외로 단순하다. "매출 / 수량으로 단가를 보여줘", "원가 빼고 이익률 띄워줘", "전년 동기 대비 증감율 컬럼 하나 추가해줘". 이런 요구를 만족시키는 방식은 크게 세 가지로 나뉜다.
첫째, **DB View 단계에서 select 절에 직접 계산식을 박는** 방법이다. `NetAmount / Quantity as UnitPrice` 같은 형태다. 짧고 빠르지만 치명적인 문제가 있다. SQL은 행 단위로 먼저 계산한 뒤 집계한다. 따라서 거래별 단가가 먼저 계산되고 그 결과가 합산되어 평균과 다른 값이 나온다. 이른바 **"집계 전 계산"** 문제다.
둘째, **Fiori 앱이나 SAC 같은 프론트에서 계산하는** 방법이다. 자유도가 높지만 동일 KPI가 여러 앱에서 반복 구현되어 결과값이 미묘하게 어긋나는 경우가 잦다. 하나의 진실(Single Source of Truth)이 깨진다.
셋째, **CDS Analytics Query 안에서 `@AnalyticsDetails.query.formula`로 선언하는** 방법이다. 이 방식은 OLAP 엔진이 "먼저 집계, 나중에 계산"을 자동으로 수행한다. 즉 `SUM(NetAmount) / SUM(Quantity)`가 런타임에 적용되며, 사용자가 드릴다운하거나 필터를 바꿔도 항상 일관된 결과가 나온다. 이번 회차가 다루는 것이 바로 이 세 번째 길이다.
---
## 2. @AnalyticsDetails.query.formula 기초 문법
`@AnalyticsDetails.query.formula`는 측정값(Element)에 붙이는 어노테이션이며, 값으로는 **문자열 형태의 수식**을 받는다. 가장 중요한 규칙은 다음과 같다.
- 수식은 반드시 **작은따옴표로 감싼 문자열**로 작성한다.
- 수식 안에 등장하는 필드명은 **같은 쿼리 뷰에 선언된 측정값**이어야 한다.
- 별도의 select 절에서 컬럼을 가져올 필요가 없다. **필드명 자체가 식별자**로 동작한다.
- 결과 컬럼명은 어노테이션이 붙은 **요소명(예: UnitPrice)** 이 그대로 사용된다.
```abap
@Analytics.query: true
define view entity ZQ_FormulaBasic
as select from ZC_SalesCube
{
Material,
@DefaultAggregation: #SUM
NetAmount,
@DefaultAggregation: #SUM
Quantity,
@AnalyticsDetails.query.formula: 'NetAmount / Quantity'
UnitPrice
}
```
위 코드에서 `UnitPrice`는 select 절에 실제 컬럼으로 존재하지 않는다. **순수히 어노테이션으로만 정의된 가상 측정값**이다. Analytic Manager가 쿼리 실행 시점에 `NetAmount`와 `Quantity`를 각각 집계한 뒤 그 결과를 나누어 `UnitPrice`를 만들어낸다.
여기서 자주 혼동되는 부분 한 가지. 요소(UnitPrice)는 select 리스트에 이름만 적어두면 충분하지만, 일부 도구 환경에서는 더미 캐스팅으로 데이터 타입을 잡아주는 패턴도 쓰인다. 예를 들어 `cast('' as abap.dec(15,2)) as UnitPrice` 같은 식이다. ADT의 신택스 체크가 까다롭게 굴면 이 방식을 고려할 만하다.
---
## 3. 사칙연산 — 단가 계산 실전 패턴
가장 흔한 패턴인 **단가 계산**을 처음부터 끝까지 따라가 본다. 비즈니스 시나리오는 명확하다. "자재별로 총 매출과 총 수량을 보여주되, 평균 판매단가도 함께 띄워라."
```abap
@DefaultAggregation: #SUM
NetAmount,
@DefaultAggregation: #SUM
Quantity,
@AnalyticsDetails.query.formula: 'NetAmount / Quantity'
UnitPrice
```
이 짧은 세 블록이 만들어내는 동작을 풀어보면 다음과 같다.
1. `NetAmount`는 합계 집계가 걸려 있다. Material로 그룹핑하면 자재별 총 매출이 된다.
2. `Quantity` 역시 합계 집계. Material 단위 총 수량이 나온다.
3. `UnitPrice`는 그 두 집계값의 나눗셈이다. **거래 라인별 단가를 평균낸 값이 아니라**, "총매출을 총수량으로 나눈 가중평균 단가"가 산출된다.
이 차이가 보고서 신뢰도를 가른다. 예를 들어 어떤 자재가 한 번은 100원에 1개 팔리고 다른 한 번은 1원에 100개 팔렸다고 하자.
- 라인별 단가 평균: (100 + 0.01) / 2 ≈ 50원
- formula 방식 가중평균: (100 + 1) / (1 + 100) ≈ 1원
수익성 분석에서 어느 쪽이 합리적인지는 자명하다. `@AnalyticsDetails.query.formula`는 본질적으로 **회계적으로 올바른 집계 후 계산**을 강제하는 메커니즘이다.
추가로 알아둘 점은, 나눗셈에는 항상 0 나눗셈 위험이 따른다는 사실이다. 운영 환경에서는 분모가 0이 될 가능성이 있다면 수식 안에 가드를 넣거나, 베이스 큐브 단계에서 0인 행을 걸러내는 전처리를 두는 것이 권장된다.
---
## 4. 실전 코드 — ZQ_SalesQuery 전체 예시 (formula 포함)
지금까지 조각으로 보여준 패턴을 하나의 완성된 쿼리 뷰로 묶어본다. 시리즈 2편에서 만들었던 `ZQ_SalesQuery`에 formula 측정값을 추가한 버전이다.
```abap
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Analytics Query with Formula'
@Analytics.query: true
@OData.publish: true
define view entity ZQ_SalesQuery
as select from ZC_SalesCube
{
// ── 행축 ───────────────────────────────
@AnalyticsDetails.query.axis: #ROWS
@AnalyticsDetails.query.display: #KEY_TEXT
Material,
@AnalyticsDetails.query.axis: #ROWS
SalesOrg,
// ── 자유 특성 ──────────────────────────
@AnalyticsDetails.query.axis: #FREE
Customer,
// ── 측정값(베이스) ─────────────────────
@AnalyticsDetails.query.axis: #COLUMNS
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'Currency'
NetAmount,
@AnalyticsDetails.query.axis: #COLUMNS
@DefaultAggregation: #SUM
@Semantics.quantity.unitOfMeasure: 'BaseUnit'
Quantity,
@AnalyticsDetails.query.axis: #COLUMNS
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'Currency'
CostAmount,
// ── 통화/단위 ──────────────────────────
@Semantics.currencyCode: true
Currency,
@Semantics.unitOfMeasure: true
BaseUnit,
// ── 계산식 측정값 ──────────────────────
@AnalyticsDetails.query.axis: #COLUMNS
@EndUserText.label: 'Unit Price'
@AnalyticsDetails.query.formula: 'NetAmount / Quantity'
UnitPrice,
@AnalyticsDetails.query.axis: #COLUMNS
@EndUserText.label: 'Profit Margin (%)'
@AnalyticsDetails.query.formula: '( NetAmount - CostAmount ) / NetAmount * 100'
ProfitMargin
}
```
이 쿼리 뷰가 Fiori Elements Analytical List Page나 Multi-Dimensional Report로 노출되면, 사용자는 별다른 설정 없이 단가 컬럼과 이익률 컬럼을 곧바로 사용할 수 있다. 드래그앤드롭으로 행축 차원을 바꿔도 두 KPI는 항상 그 시점의 집계 상태에 맞춰 재계산된다.
주의해서 볼 부분이 두 가지 있다. 첫째, `NetAmount`와 `CostAmount`처럼 통화가 들어가는 측정값에는 `@Semantics.amount.currencyCode`가 붙어 있어야 단위 처리가 일관된다. 둘째, formula 측정값에는 굳이 `@DefaultAggregation`을 붙이지 않는다. 어차피 베이스 측정값이 먼저 집계된 결과를 받아 연산하기 때문이다.
---
## 5. 복합 KPI — 이익률과 증감율 계산
단순 사칙연산을 넘어 복합 수식을 다뤄본다. 실무에서 가장 자주 등장하는 두 KPI를 골랐다.
### 5.1 이익률 (Profit Margin)
```abap
@AnalyticsDetails.query.formula:
'( NetAmount - CostAmount ) / NetAmount * 100'
ProfitMargin
```
수식의 의미는 단순하다. (매출 - 원가) / 매출 × 100. 그러나 여기서도 "집계 후 계산"이 위력을 발휘한다. 자재별 이익률, 영업조직별 이익률, 고객별 이익률 — 사용자가 어떤 차원으로 드릴다운하든 분자와 분모가 그 그룹 안에서 먼저 합산된 뒤 비율이 산출된다. 결과적으로 "전사 평균 이익률"과 "자재별 평균 이익률을 단순 평균낸 값"이 일치하지 않는 흔한 함정을 피할 수 있다.
### 5.2 전기 대비 증감율 (Growth Rate)
```abap
@AnalyticsDetails.query.formula:
'( NetAmount - PrevNetAmount ) / PrevNetAmount * 100'
GrowthRate
```
이 수식은 한 단계 전제가 필요하다. `PrevNetAmount`라는 측정값이 이미 큐브 뷰 또는 쿼리 뷰 안에 정의되어 있어야 한다. 일반적으로 베이스 큐브 단계에서 시간 이동(time shift) 로직을 통해 전년 동월/전월 값을 별도 컬럼으로 만들어 두는 패턴을 쓴다.
증감율 수식에서 가장 골치 아픈 케이스는 `PrevNetAmount = 0`이다. 신규 자재나 신규 고객일 때 발생한다. 어노테이션 수식만으로는 CASE 분기를 쓰기 어렵기 때문에, 보통 다음 둘 중 하나를 선택한다.
- 큐브 단계에서 `PrevNetAmount`가 0이면 NULL로 치환 (NULLIF 패턴)
- 결과를 받는 프론트(SAC/Fiori)에서 무한대 값 표시를 "—"로 마스킹
어느 쪽이든 **수식 자체는 단순하게 유지**하는 것이 권장된다.
---
## 6. @DefaultAggregation과의 관계 및 주의사항
`@AnalyticsDetails.query.formula`를 처음 다룰 때 가장 자주 막히는 지점이 바로 집계와의 관계다. 핵심 원칙은 다음과 같다.
> **베이스 측정값은 먼저 집계된다. formula 측정값은 그 집계 결과 위에서 연산된다.**
따라서 다음 규칙이 따라온다.
1. **formula의 입력으로 쓰이는 측정값에는 `@DefaultAggregation`이 반드시 정의**되어 있어야 한다. (보통 `#SUM`)
2. **formula 측정값 자체에는 `@DefaultAggregation`을 붙이지 않는다.** 굳이 붙이더라도 의미가 없거나 무시된다.
3. **formula 안에서 또 다른 formula 측정값을 참조하는 것은 권장되지 않는다.** 가능하더라도 가독성이 급격히 떨어지고 디버깅이 어려워진다. 중간 단계 계산이 필요하면 베이스 큐브에 별도 측정값으로 빼는 편이 낫다.
4. **단위/통화 일관성**에 유의한다. `NetAmount`가 EUR이고 `CostAmount`가 USD인 상태에서 그대로 빼면 결과가 무의미하다. 큐브 단계에서 통화 변환을 마쳐두거나, `@Semantics.amount.currencyCode`로 묶어 변환 함수를 거치게 해야 한다.
또 하나, formula 측정값은 **데이터베이스 푸시다운이 다소 제한**될 수 있다. 베이스 측정값까지는 HANA가 직접 SUM을 수행하지만, 그 뒤 나눗셈/곱셈은 Analytic Engine 레벨에서 처리되는 경우가 일반적이다. 데이터 볼륨이 매우 크고 계산식이 복잡하다면, 차라리 큐브 뷰에 사전 계산 컬럼을 두거나 CDS Table Function을 활용하는 대안을 검토해볼 만하다.
---
## 7. 자주 발생하는 오류와 해결법
### Q1. "Field XYZ is unknown" 액티베이션 에러가 난다
수식 안에 적은 필드명이 같은 쿼리 뷰에서 식별되지 않을 때 나타난다. 점검할 포인트는 세 가지다.
- select 리스트에 해당 측정값이 실제로 포함되어 있는가
- 별칭(alias)을 썼다면 원래 컬럼명이 아닌 **별칭을 수식에 적었는지**
- 베이스 큐브 뷰에서 measure로 노출되어 있는가
특히 별칭은 사고가 잦다. 큐브 뷰에서 `NetAmount as Revenue`로 별칭을 줬다면 쿼리 뷰의 수식도 `'Revenue / Quantity'`로 적어야 한다.
### Q2. 결과 값이 NULL 또는 0으로만 나온다
대부분 두 가지 원인이다. 첫째, 분모로 쓰인 측정값이 모두 0이거나 NULL인 경우. 둘째, formula 측정값이 행축에 잘못 배치된 경우다. `@AnalyticsDetails.query.axis: #COLUMNS`가 빠지면 의도와 다른 자리로 가서 결과가 비어 보일 수 있다.
### Q3. Fiori 앱에서 단가/이익률 컬럼이 보이지 않는다
쿼리 뷰는 액티베이션되었는데 앱에서 노출되지 않는 경우, OData 노출 설정과 UI 어노테이션을 함께 점검해야 한다. `@UI.lineItem`이나 `@UI.dataPoint`에 해당 요소가 포함되어 있는지, 또는 메타데이터 캐시가 갱신되었는지 확인한다. 종종 게이트웨이 캐시(`/IWFND/CACHE_CLEANUP`) 한 번이면 해결된다.
### Q4. 0 나눗셈 오류가 사용자 화면에 그대로 노출된다
운영 베스트 프랙티스는 베이스 큐브 단계에서 분모가 될 측정값에 대해 NULLIF 처리를 적용하거나, 0인 라인을 사전에 필터링하는 것이다. 어노테이션 수식 자체에 복잡한 분기를 넣기 시작하면 유지보수가 어려워진다.
### Q5. 동일 KPI인데 보고서마다 값이 다르게 나온다
거의 100% **계산 위치 불일치** 문제다. 어떤 보고서는 큐브 단계에서 계산하고, 어떤 보고서는 프론트에서 계산하는 식이다. 해결책은 단순하다. **계산식 측정값을 쿼리 뷰에 단일 정의**하고 모든 보고서가 그 쿼리를 참조하도록 표준화한다. 이것이 `@AnalyticsDetails.query.formula`의 본질적 가치다.
---
## 8. 마무리 — formula로 완결되는 분석 모델
여기까지 오면 분석 쿼리 한 장에 담을 수 있는 것이 한층 풍부해진다. 차원과 측정값을 배치(2편)하고, 사용자 입력을 받아(3편), 거기에 비즈니스 로직이 녹아든 계산식 KPI(4편)까지 한 번에 선언할 수 있다. SQL을 한 줄도 더 쓰지 않고 어노테이션만으로 가중평균 단가, 이익률, 증감율을 완성한 것이다.
`@AnalyticsDetails.query.formula`가 가진 진짜 가치는 두 가지로 요약된다.
- **계산 위치의 표준화**: KPI 정의가 CDS 안에 한 번만 살아 있으면, Fiori든 SAC든 Embedded Analytics든 동일한 값을 본다. 부서 간 보고서 숫자가 어긋나는 고질적 문제를 구조적으로 해결한다.
- **집계 의미론의 정확성**: 항상 "집계 후 계산"이 보장되므로, 단순 평균이 아닌 가중 평균이 자연스럽게 도출된다. 회계·재무 관점에서 옳은 숫자가 디폴트로 나온다.
다음 5편(시리즈 마지막)에서는 `@ObjectModel.text.element`와 `@Semantics.text: true`로 코드-텍스트 매핑을 선언적으로 처리하는 패턴을 다룬다. 이번 편의 formula와 결합하면, 코딩 없이도 실무 분석 보고서의 핵심을 CDS 한 장으로 완결할 수 있는 수준에 도달한다.
코드 한 줄로 표현된 어노테이션이 ERP 전반의 보고 일관성을 떠받친다는 사실 — 이것이 ABAP CDS Analytics Query가 가진 가장 조용하지만 강력한 무기다.
댓글 0
아직 댓글이 없습니다.