ABAP

ROWS vs COLUMNS vs FREE — CDS Query View axis 설계 #shorts #SAP #ABAP

▶ YouTube에서 보기

이 글이 답하는 질문

CDS Query View를 작성했는데 Fiori Elements Analytical List Page에서 모든 필드가 한 줄로 나열되거나, Measure가 행으로 빠져버려서 피벗이 깨진 경험이 있나요? 원인은 대부분 @AnalyticsDetails.query.axis 어노테이션을 지정하지 않았기 때문입니다. axis는 Query View의 각 필드가 분석 화면에서 행, 열, 자유 필터 중 어디에 배치될지를 미리 선언하는 장치입니다.

  • axis 어노테이션이 없으면 어떤 기본 동작이 적용되는가
  • #ROWS, #COLUMNS, #FREE 각각이 표현하는 분석 의미는 무엇인가
  • Dimension과 Measure를 어떤 axis에 배치해야 자연스러운 피벗이 되는가
  • FREE axis로 분류된 필드는 사용자 화면에서 어떻게 노출되는가

이 글을 보기 전에

이 글은 ABAP CDS Analytical Query 시리즈의 4번째입니다. 사전 지식으로 다음 항목이 필요합니다.

  • CUBE 뷰의 역할 (@Analytics.dataCategory: #CUBE)과 FACT/DIMENSION 분리 모델
  • Measure 필드에 적용되는 @DefaultAggregation 동작 (#SUM, #MIN, #MAX, #AVG)
  • Query 뷰 자체의 정의 (@Analytics.query: true)와 그 위에 얹히는 분석 런타임
  • Association 기반 외래키 연결과 $projection 노출 패턴

테스트 환경

이 글의 코드는 ABAP for Cloud Development (ABAP BTP, RAP 환경)와 S/4HANA 2022 이상의 ABAP for Key Users 환경에서 동일하게 동작한다고 알려져 있습니다. 다만 어노테이션 일부가 릴리스에 따라 활성화 여부가 다를 수 있으므로 ADT의 Annotation propagation view에서 적용 여부를 확인하는 것이 일반적으로 권장됩니다.

  • 개발 도구: Eclipse + ADT (ABAP Development Tools) 2024-03 이상
  • 백엔드: ABAP Cloud (Steampunk) 또는 S/4HANA 2022 FPS01 이상
  • 프리뷰 도구: ADT 우클릭 Open With > Data Preview, 혹은 Fiori Elements Analytical List Page
  • 샘플 데이터: 판매 주문 항목 기반의 사용자 정의 CUBE (ZC_SalesCube)
  • 네임스페이스: Z* (커스텀 객체) 가정
버전이 다르면 동일한 어노테이션이라도 Default 동작이 미세하게 다를 수 있으므로, 한 시스템에서 정상이던 Query가 다른 시스템에서 다르게 보일 때는 axis 어노테이션의 명시 여부부터 점검하는 것이 일반적입니다.

핵심 개념: axis가 분석 결과를 결정하는 이유

Analytical Query는 단순한 SELECT 결과 집합이 아니라 다차원 큐브에 대한 피벗 정의입니다. 사용자 화면에서 행과 열이 교차하면서 셀에 집계값이 들어가는 구조이기 때문에, 각 필드가 행 헤더가 될지 열 헤더가 될지 측정값이 될지 사전에 분류되어야 합니다.

이 분류 역할을 하는 것이 @AnalyticsDetails.query.axis 어노테이션이며, 다음 세 값을 가집니다.

axis 값의미전형적인 필드
#ROWS피벗의 행 헤더로 표시Country, Customer, Material 등 Dimension
#COLUMNS피벗의 열 또는 측정 영역에 표시NetAmount, Quantity 등 Measure
#FREE화면 초기에는 표시되지 않고, 필터 패널에만 노출FiscalYear, CompanyCode 같은 보조 분류

비유하자면 엑셀 피벗 테이블의 행 영역, 값 영역, 필터 영역과 거의 1:1로 대응됩니다. ROWS는 왼쪽 세로축 라벨, COLUMNS는 가로축 값, FREE는 상단의 슬라이서라고 생각하면 직관적입니다.

axis 어노테이션을 한 필드도 지정하지 않으면 분석 런타임이 모든 필드를 가능한 한 표시하려고 하기 때문에, 의도하지 않은 컬럼 배치가 나타나거나 Measure가 차원처럼 보이는 현상이 발생합니다. 따라서 Query 뷰의 모든 노출 필드는 가능하면 axis를 명시적으로 선언하는 것이 일반적으로 권장됩니다.

1단계 — axis #ROWS: Dimension 필드를 행으로 배치

먼저 기반이 되는 CUBE 뷰의 형태를 살펴보면 다음과 같습니다. 이 글에서는 이미 존재한다고 가정합니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@Analytics.dataCategory: #CUBE
@EndUserText.label: 'Sales Cube'
define view entity ZC_SalesCube
  as select from ZI_SalesItemFact as Fact
  association [0..1] to ZI_CountryDim as _Country
    on $projection.CountryCode = _Country.CountryCode
{
  key Fact.SalesOrder,
  key Fact.SalesOrderItem,
      Fact.CountryCode,
      Fact.CustomerID,
      Fact.CompanyCode,
      Fact.FiscalYear,
      @DefaultAggregation: #SUM
      Fact.NetAmount,
      @DefaultAggregation: #SUM
      Fact.Quantity,
      _Country
}

이 CUBE를 기반으로 Query 뷰의 1단계 버전을 만듭니다. 우선 Dimension 한 개만 ROWS로 지정하여 가장 단순한 피벗을 형성합니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@Analytics.query: true
@EndUserText.label: 'Sales Query - Step 1'
define view entity ZQ_SalesQuery_S1
  as select from ZC_SalesCube
{
  @AnalyticsDetails.query.axis: #ROWS
  @AnalyticsDetails.query.displayHierarchy: #ROWS
  CountryCode,

  @AnalyticsDetails.query.axis: #COLUMNS
  NetAmount
}

이 단계에서 ADT Data Preview를 열면 국가 코드가 왼쪽 세로 라벨로, NetAmount 합계가 오른쪽 값으로 표시됩니다. ROWS axis 덕분에 CountryCode가 차원 라벨로 인식되었고, Measure는 자동으로 SUM으로 집계됩니다.

2단계 — axis #COLUMNS: Measure 집계값을 열로 배치

실무에서는 단일 Measure만으로 분석이 끝나지 않습니다. NetAmount와 Quantity를 동시에 비교하고, 행에는 Country와 Customer를 함께 두는 형태가 일반적입니다. 두 개 이상의 Measure를 COLUMNS axis로 명시하면 분석 화면에서 측정값 영역이 그룹으로 묶입니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@Analytics.query: true
@EndUserText.label: 'Sales Query - Step 2'
define view entity ZQ_SalesQuery_S2
  as select from ZC_SalesCube
{
  @AnalyticsDetails.query.axis: #ROWS
  CountryCode,

  @AnalyticsDetails.query.axis: #ROWS
  CustomerID,

  @AnalyticsDetails.query.axis: #COLUMNS
  @EndUserText.label: 'Net Amount'
  NetAmount,

  @AnalyticsDetails.query.axis: #COLUMNS
  @EndUserText.label: 'Quantity'
  Quantity
}

이때 ADT 우측 Properties 뷰에서 각 필드의 Annotation propagation을 확인하면, CUBE에서 정의한 @DefaultAggregation: #SUM이 그대로 상속되어 NetAmount와 Quantity 모두 합계 집계로 동작함을 알 수 있습니다. 만약 평균값이 필요하다면 Query 뷰에서 다음과 같이 재선언하는 패턴도 사용합니다.

  @AnalyticsDetails.query.axis: #COLUMNS
  @DefaultAggregation: #AVG
  @EndUserText.label: 'Avg Net Amount'
  NetAmount as AvgNetAmount

실무 팁으로, COLUMNS axis에 너무 많은 Measure를 한꺼번에 올리면 ALP에서 가로 스크롤이 크게 늘어나 사용성이 떨어집니다. 일반적으로 한 화면에 3~5개 Measure 정도로 제한하고 나머지는 별도의 보조 Query 뷰로 분리하는 것이 권장됩니다.

3단계 — axis #FREE: 화면 미표시 필터 전용 필드

회계연도(FiscalYear)나 회사코드(CompanyCode)처럼 분석 컨텍스트로는 중요하지만 항상 화면에 보이게 두면 피벗이 너무 세분화되는 필드가 있습니다. 이런 필드는 #FREE로 선언하여 초기 화면에서는 숨기고 필터 패널에만 노출되도록 설계합니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@Analytics.query: true
@EndUserText.label: 'Sales Query - Step 3'
@Metadata.allowExtensions: true
define view entity ZQ_SalesQuery
  as select from ZC_SalesCube
{
  @AnalyticsDetails.query.axis: #ROWS
  @AnalyticsDetails.query.displayHierarchy: #ROWS
  @EndUserText.label: 'Country'
  CountryCode,

  @AnalyticsDetails.query.axis: #ROWS
  @EndUserText.label: 'Customer'
  CustomerID,

  @AnalyticsDetails.query.axis: #COLUMNS
  @EndUserText.label: 'Net Amount'
  NetAmount,

  @AnalyticsDetails.query.axis: #COLUMNS
  @EndUserText.label: 'Quantity'
  Quantity,

  @AnalyticsDetails.query.axis: #FREE
  @Consumption.filter: { selectionType: #SINGLE,
                         multipleSelections: false,
                         mandatory: false }
  @EndUserText.label: 'Fiscal Year'
  FiscalYear,

  @AnalyticsDetails.query.axis: #FREE
  @Consumption.filter: { selectionType: #INTERVAL,
                         multipleSelections: true,
                         mandatory: false }
  @EndUserText.label: 'Company Code'
  CompanyCode
}

이 형태가 시리즈에서 목표로 한 완성형 Query 뷰입니다. 행에는 Country/Customer, 열 영역에는 NetAmount/Quantity, 자유 필터에는 FiscalYear/CompanyCode가 배치됩니다. 사용자는 처음 화면을 열면 국가-고객별 매출과 수량 피벗을 보고, 필요하면 필터 바에서 회계연도나 회사코드를 좁혀 분석을 이어갈 수 있습니다.

프로덕션 환경에서는 다음 추가 패턴이 함께 사용됩니다.

  • @AccessControl.authorizationCheck: #CHECK로 변경 후 DCL(Data Control Language) 적용
  • Service Binding을 OData V4로 발행하여 Fiori Elements ALP가 axis 메타데이터를 읽도록 구성
  • 대용량 Cube의 경우 FREE axis 필드에 @Consumption.filter.mandatory: true를 부여하여 첫 호출에서 풀스캔을 방지
  • 단위 테스트는 ABAP Unit + cl_cds_test_environment로 데이터 격리 후 집계 결과를 검증

삽질 노트

FAQ 1. axis를 하나도 선언하지 않으면 무슨 일이 생기나요?
일반적으로 Dimension은 #ROWS, Measure는 #COLUMNS로 추정되어 동작하지만, Query 뷰 내에서 필드 순서나 어노테이션 조합에 따라 결과가 달라질 수 있습니다. 특히 FREE에 들어가야 할 필드가 ROWS로 잡혀 화면이 과도하게 세분화되는 사례가 흔합니다. 가능한 한 모든 노출 필드에 명시적으로 axis를 지정하는 것이 권장됩니다.

FAQ 2. FREE axis로 지정했는데 화면에 그대로 컬럼이 보이는 이유는?
ALP 변형(Variant)에 이전 상태가 저장되어 있으면 어노테이션 변경이 즉시 반영되지 않습니다. ADT에서 Service Binding 재활성화 후 브라우저 캐시와 사용자 Variant를 초기화한 뒤 다시 확인해야 합니다. 또한 Fiori Elements 측 manifest에 컬럼 강제 노출 설정이 있으면 axis보다 우선될 수 있습니다.

FAQ 3. Measure를 #ROWS로 두면 어떻게 되나요?
드물게 Measure를 행으로 표시해 비교하는 BI 패턴이 있긴 하지만, ABAP CDS Analytical Query에서는 일반적인 디자인이 아닙니다. 집계 의미가 모호해지고 ALP의 차트 컴포넌트가 인식하지 못하는 경우가 많아 권장되지 않습니다. 비교가 필요하면 별도의 Calculated Measure를 정의하는 편이 안전합니다.

FAQ 4. axis 외에 함께 자주 쓰는 어노테이션은?
@AnalyticsDetails.query.displayHierarchy로 행 트리 펼침 동작을 제어하고, @AnalyticsDetails.query.totals로 합계 표시 여부를 지정합니다. 이 둘을 axis와 같이 묶어 설계하면 BI 화면 완성도가 크게 올라갑니다.

다음 단계 / 관련 주제

시리즈의 마지막 5편에서는 Query View 위에 얹는 Parameter, Variable, Input Parameter 및 @Consumption.filter의 정교한 조합을 다룰 예정입니다. axis 설계가 끝났다면 다음 주제도 함께 살펴보면 좋습니다.

  • Restricted Measure와 Calculated Measure로 동일 Cube에서 여러 KPI 파생
  • Hierarchy CDS와 ROWS axis의 displayHierarchy 연동
  • OData V4 Service Binding + Fiori Elements ALP의 차트/테이블 자동 생성
  • SAP Analytics Cloud(SAC) Live Connection을 통한 Cube/Query 직접 소비

참고 자료

댓글 0

아직 댓글이 없습니다.