News

환율 SUM이 틀린 이유 — CDS Query View Exception Aggregation #shorts #SAP #ABAP

▶ YouTube에서 보기

1. 이 글이 답하는 질문 — 왜 환율을 SUM으로 집계하면 틀리는가

ABAP CDS Analytics Query View에서 측정값(Measure)은 기본적으로 SUM으로 집계됩니다. 매출, 수량, 금액처럼 단순 합산이 의미를 가지는 필드는 문제가 없지만, 환율·단가·재고·평균치 같은 필드는 SUM으로 더하면 결과가 완전히 어긋납니다. 예를 들어 1.1, 1.15, 1.12 라는 USD→EUR 환율 3건을 SUM 하면 3.37 이라는 무의미한 숫자가 나옵니다.

이 글에서는 다음 질문에 답합니다.

  • SUM이 부적절한 측정값을 어떻게 분리해서 집계할 것인가
  • @DefaultAggregation@AnalyticsDetails.query.exceptionAggregationSteps는 어떻게 다른가
  • LAST, MAX, MIN, AVG 같은 예외 집계를 선언하는 정확한 어노테이션 구조
  • exceptionAggregationElements로 Dimension 기준축을 어떻게 지정하는가
  • S/4HANA Embedded Analytics에서 환율 환산 시나리오에 어떻게 적용하는가

대상 독자는 CDS View Entity 기반 Analytical Query(@Analytics.query: true)를 한 번이라도 만들어 본 ABAP 개발자입니다.

이 글을 보기 전에

  • CDS View / View Entity 의 기본 문법 (define view entity, @Analytics)
  • Cube View와 Query View의 역할 분리 — Query는 Cube를 reuse하여 표현 계층을 정의
  • @DefaultAggregation: #SUM 같은 기본 집계 어노테이션의 의미
  • S/4HANA 2022 이상 또는 BTP ABAP Environment의 Analytical Engine 동작 방식에 대한 기본 이해

2. Exception Aggregation이란 — DefaultAggregation과의 관계

Default Aggregation은 측정값이 모든 Dimension에 대해 기본적으로 어떻게 합쳐질지 지정합니다. SUM, MAX, MIN, AVG, COUNT 같은 값이 가능합니다.

Exception Aggregation은 그 기본 집계 위에 한 단계를 더 얹어, "특정 Dimension에 대해서는 다른 방식으로 집계하라"고 선언합니다. 즉 2단계 집계 파이프라인입니다.

비유하자면, DefaultAggregation은 "기본 요리법", ExceptionAggregation은 "특정 재료에만 적용하는 특별 조리법"입니다. 모든 야채는 굽지만(SUM), 마늘만은 별도로 다지듯(LAST) 처리하는 식입니다.

처리 순서는 다음과 같이 이해할 수 있습니다.

  1. 먼저 exceptionAggregationElements에 지정된 Dimension을 기준으로 그룹핑하여 Exception Aggregation 함수를 적용합니다. (예: 통화쌍별로 LAST 환율 선택)
  2. 그 결과를 나머지 Dimension에 대해 DefaultAggregation으로 집계합니다. (예: 그렇게 선택된 환율을 SUM/AVG로 합산)

S/4HANA Analytical Engine은 이 순서를 보장하므로, "통화별 마지막 환율을 먼저 잡고, 그 다음 다른 축으로 합산"이라는 비즈니스 요구를 어노테이션만으로 표현할 수 있습니다.

3. exceptionAggregationSteps 기본 선언 구조

핵심 어노테이션은 @AnalyticsDetails.query.exceptionAggregationSteps 입니다. 배열 형태이며 각 step은 다음 필드를 가집니다.

  • exceptionAggregationBehavior — 집계 함수: #LAST, #FIRST, #MAX, #MIN, #AVERAGE, #STANDARD_DEVIATION, #COUNT, #NONE
  • exceptionAggregationElements — Dimension(필드명) 배열. 어떤 축을 기준으로 예외 집계를 적용할지 지정

가장 단순한 형태의 선언입니다.

@DefaultAggregation: #SUM
@AnalyticsDetails.query.exceptionAggregationSteps: [
  {
    exceptionAggregationBehavior: #LAST,
    exceptionAggregationElements: [ 'CalendarDay' ]
  }
]
ExchangeRate

해석: ExchangeRate 필드는 기본적으로 SUM이지만, CalendarDay 축에 대해서는 마지막 값(LAST)만 선택하라는 의미입니다.

4. LAST — 환율·단가 필드에 마지막 값 적용

환율(Exchange Rate), 단가(Unit Price), 평균재고단가(Moving Average Price)는 시간축에서 가장 최근 값이 의미가 있습니다. #LASTexceptionAggregationElements 에 지정된 Dimension의 가장 큰 값(보통 날짜)에 해당하는 측정값을 선택합니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Currency Conversion - Query'
@Analytics.query: true
@OData.publish: true

define view entity Z_C_CurrConv_Q
  as select from Z_C_CurrConv_CUBE
{
  @AnalyticsDetails.query.axis: #ROWS
  SourceCurrency,

  @AnalyticsDetails.query.axis: #ROWS
  TargetCurrency,

  @AnalyticsDetails.query.axis: #COLUMNS
  CalendarDay,

  @DefaultAggregation: #SUM
  @AnalyticsDetails.query.exceptionAggregationSteps: [
    {
      exceptionAggregationBehavior: #LAST,
      exceptionAggregationElements: [ 'CalendarDay' ]
    }
  ]
  @Semantics.quantity.unitOfMeasure: 'SourceCurrency'
  ExchangeRate
}

이 선언 덕분에, 행 축에 SourceCurrency/TargetCurrency 만 두고 CalendarDay 를 빼더라도 "마지막 일자의 환율" 한 건이 자동으로 선택됩니다. SUM으로 환율 3.37 같은 값이 나오는 사고를 막을 수 있습니다.

5. MAX·MIN — 재고·가격 극값 집계

재고 스냅샷(Stock Snapshot)이나 일별 종가/최저가 같은 필드는 SUM도 LAST도 어울리지 않는 경우가 있습니다. 이때는 #MAX, #MIN 을 씁니다.

define view entity Z_C_Inventory_Q
  as select from Z_C_Inventory_CUBE
{
  @AnalyticsDetails.query.axis: #ROWS
  Material,

  @AnalyticsDetails.query.axis: #ROWS
  Plant,

  @AnalyticsDetails.query.axis: #FREE
  CalendarMonth,

  // 월별 최대 재고
  @DefaultAggregation: #SUM
  @AnalyticsDetails.query.exceptionAggregationSteps: [
    {
      exceptionAggregationBehavior: #MAX,
      exceptionAggregationElements: [ 'CalendarMonth' ]
    }
  ]
  @Semantics.quantity.unitOfMeasure: 'BaseUnit'
  MaxMonthlyStock,

  // 월별 최저 재고
  @DefaultAggregation: #SUM
  @AnalyticsDetails.query.exceptionAggregationSteps: [
    {
      exceptionAggregationBehavior: #MIN,
      exceptionAggregationElements: [ 'CalendarMonth' ]
    }
  ]
  @Semantics.quantity.unitOfMeasure: 'BaseUnit'
  MinMonthlyStock,

  @Semantics.unitOfMeasure: true
  BaseUnit
}

이렇게 선언하면 Plant/Material 단위로 Drilldown 했을 때, 각 월별로 먼저 Max/Min을 잡고, 그 다음 다른 축(예: 자재 그룹)에 대해 SUM으로 합산하는 2단계 처리가 자동으로 적용됩니다.

6. exceptionAggregationElements — Dimension 배열 지정

exceptionAggregationElements 는 단일 필드뿐 아니라 복수 필드 배열도 받습니다. 어떤 조합을 기준축으로 둘지가 결과를 결정합니다.

// 통화쌍 × 날짜 조합으로 LAST 적용
@AnalyticsDetails.query.exceptionAggregationSteps: [
  {
    exceptionAggregationBehavior: #LAST,
    exceptionAggregationElements: [
      'SourceCurrency',
      'TargetCurrency',
      'CalendarDay'
    ]
  }
]

주의할 점은, 여기에 지정된 Dimension은 "이 축의 마지막 값을 골라낸다"는 뜻이지 "이 축으로 묶는다"는 뜻이 아니라는 것입니다. 실제 그룹핑은 Query에 포함된 다른 Dimension이 결정합니다.

여러 step을 배열로 연속 선언할 수도 있어, 단계별 집계 파이프라인을 구성할 수 있습니다. 다만 일반적으로 step 1~2개로 표현 가능한 비즈니스 요구가 대부분이며, 복잡한 다단 집계는 Cube에서 미리 계산해두는 것이 일반적으로 권장됩니다.

step 처리 순서

  • 배열의 첫 번째 step부터 순서대로 적용됨
  • 각 step의 결과가 다음 step의 입력이 됨
  • 모든 step이 끝난 뒤 DefaultAggregation으로 최종 집계됨

7. 실전 예제 — 환율 환산이 올바른 Query View 전체 패턴

실제 시나리오 — 매출(Revenue)을 거래통화에서 회사통화로 환산하여 조회하는 Query View입니다. 환율은 LAST, 매출은 SUM으로 처리해야 합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Revenue with Currency Conversion'
@Analytics.query: true
@OData.publish: true
@VDM.viewType: #CONSUMPTION

define view entity Z_C_Revenue_FX_Q
  as select from Z_C_Revenue_CUBE
{
  // --- Dimensions ---
  @AnalyticsDetails.query.axis: #ROWS
  @AnalyticsDetails.query.display: #KEY_TEXT
  CompanyCode,

  @AnalyticsDetails.query.axis: #ROWS
  CustomerGroup,

  @AnalyticsDetails.query.axis: #FREE
  CalendarMonth,

  @AnalyticsDetails.query.axis: #FREE
  TransactionCurrency,

  @AnalyticsDetails.query.axis: #FREE
  CompanyCodeCurrency,

  // --- Raw Measure ---
  @DefaultAggregation: #SUM
  @Semantics.amount.currencyCode: 'TransactionCurrency'
  RevenueAmountTC,

  // --- Exchange Rate (LAST per currency pair / month) ---
  @DefaultAggregation: #SUM
  @AnalyticsDetails.query.exceptionAggregationSteps: [
    {
      exceptionAggregationBehavior: #LAST,
      exceptionAggregationElements: [
        'TransactionCurrency',
        'CompanyCodeCurrency',
        'CalendarMonth'
      ]
    }
  ]
  ExchangeRate,

  // --- Converted Measure (Formula) ---
  @DefaultAggregation: #FORMULA
  @AnalyticsDetails.query.formula: 'RevenueAmountTC * ExchangeRate'
  @Semantics.amount.currencyCode: 'CompanyCodeCurrency'
  RevenueAmountCC,

  @Semantics.currencyCode: true
  TransactionCurrency,

  @Semantics.currencyCode: true
  CompanyCodeCurrency
}

구조 해석.

  • RevenueAmountTC — 거래통화 매출. 순수 SUM.
  • ExchangeRate — 통화쌍·월 단위로 LAST. 같은 월에 환율이 여러 건 있어도 가장 최근 값만 선택.
  • RevenueAmountCC — Formula로 두 측정값을 곱함. @DefaultAggregation: #FORMULA 가 핵심으로, "먼저 각 행의 환산값을 계산한 뒤 SUM" 이 아니라 "먼저 SUM과 LAST를 각각 적용한 뒤 곱한다"는 의미가 됩니다.

이 패턴은 S/4HANA Embedded Analytics의 환산 시나리오에서 일반적으로 권장되는 구조이며, Analysis for Office나 SAP Analytics Cloud Live Connection에서 일관된 숫자를 보여줍니다.

테스트 방법

  • RSRT 또는 ADT의 Data Preview에서 Drilldown 축을 바꿔가며 환율 값이 변하지 않는지 확인
  • CalendarDay 를 축에서 제거했을 때 환율이 SUM(3.37 같은 값)이 되지 않는지 검증
  • Trace(ST05/RSTT)로 HANA Calculation View로 push-down 되는지 확인

8. 흔한 실수와 디버깅 포인트

실수 1 — DefaultAggregation을 빠뜨림

exceptionAggregationSteps만 선언하고 @DefaultAggregation 을 생략하는 경우가 많은데, 이 경우 활성화 자체가 실패하거나 의도와 다른 NONE 처리가 됩니다. 일반적으로 두 어노테이션은 짝으로 선언하는 것이 권장됩니다.

실수 2 — exceptionAggregationElements에 Measure 지정

exceptionAggregationElements 에는 반드시 Dimension(특성)만 들어가야 합니다. 측정값 필드명을 넣으면 활성화 오류가 발생합니다. "어떤 축의 마지막 값이냐"를 정의하는 자리임을 기억해야 합니다.

실수 3 — Formula를 SUM으로 잘못 선언

환산 매출처럼 두 측정값의 곱은 반드시 @DefaultAggregation: #FORMULA 로 선언해야 합니다. SUM으로 두면 "행별로 곱한 뒤 합" 처럼 잘못된 push-down이 발생할 수 있습니다.

실수 4 — Cube에서 미리 SUM 처리

Cube 단계에서 환율을 미리 SUM/AVG로 집계해버리면 Query에서 아무리 LAST를 선언해도 이미 손상된 값에 대한 LAST가 됩니다. Exception Aggregation 대상 필드는 Cube에서 raw 형태로 expose 해야 합니다.

FAQ

  • Q. #LAST와 #MAX는 같은가? — 시간축에서 가장 큰 날짜를 기준으로 한다는 점에서 비슷해 보이지만, MAX는 측정값 자체의 최대치, LAST는 지정 Dimension의 가장 큰 값에 대응하는 측정값입니다. 환율은 LAST가 일반적으로 맞습니다.
  • Q. AVG는 왜 잘 안 쓰는가? — 환율 평균은 비즈니스적으로 모호한 경우가 많아 LAST가 우선됩니다. 다만 KPI성 지표(평균 처리시간 등)에는 #AVERAGE 가 유효합니다.
  • Q. Step을 여러 개 쌓을 수 있는가? — 가능합니다. 다만 단계가 깊어지면 push-down 성능이 떨어지는 경향이 있어, 3단계 이상은 Cube에서 분리하는 것이 일반적으로 권장됩니다.

디버깅 체크리스트

  • ADT에서 Query를 활성화한 뒤 Data Preview 로 축을 추가/제거해보며 측정값 변동 확인
  • RSRT 트랜잭션에서 Execute + Debug 로 OLAP 처리 trace 확인
  • HANA Studio의 PlanViz로 Exception Aggregation 노드가 생성되는지 확인
  • Currency Conversion이 함께 걸려있다면 @Semantics.amount.currencyCode 가 올바른 통화 필드를 가리키는지 점검

Exception Aggregation은 환율·재고·평균치처럼 "단순 합산이 답이 아닌" 측정값을 다루는 Analytical Query의 필수 도구입니다. 이 글의 패턴을 그대로 두고 필드명만 바꿔도 대부분의 환산/스냅샷 시나리오에 적용 가능합니다.

댓글 0

아직 댓글이 없습니다.