News

CDS 쿼리 왜 느리지? 성능 힌트 어노테이션 완성 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. 개요 및 이 글의 목표

ABAP CDS(Core Data Services)는 단순한 데이터베이스 뷰 정의 언어가 아니라, SAP HANA 옵티마이저에게 의도를 전달하는 선언적 메타데이터 계층이다. CDS 어노테이션을 적절히 활용하면 같은 SELECT 문이라도 실행 계획이 달라지고, 응답 시간이 수십 배 개선되거나 반대로 악화될 수 있다.

본 글에서는 ABAP 개발자가 실무에서 가장 자주 마주치는 3종의 성능 관련 어노테이션 — @AbapCatalog.buffering, @AbapCatalog.dbHints, @ObjectModel.usageType — 을 중심으로 어떻게 HANA 옵티마이저를 유도하고, Plan Visualizer로 효과를 검증하며, 잘못 설정했을 때 어떤 함정에 빠지는지 다룬다. 점검 체크리스트는 다음과 같다.

  • 버퍼링 가능 여부 판단 기준과 단일/일반/전체 버퍼링 선택
  • dbHints로 HANA에 JOIN 순서/엔진 선택 강제
  • usageType으로 OData/Analytic Manager의 뷰 사용 방식 선언
  • HANA Plan Visualizer를 활용한 실행 계획 비교 검증

2. 핵심 개념 — CDS 성능 힌트란 무엇인가

HANA의 옵티마이저는 통계 기반(cost-based)으로 동작하지만, 모든 워크로드 패턴을 자동으로 추론하지는 못한다. 특히 다음 상황에서 옵티마이저가 잘못된 결정을 내릴 가능성이 높다.

  • JOIN 대상 테이블의 selectivity가 통계와 다르게 분포할 때
  • 중첩 뷰(view on view) 깊이가 깊어 펼친 SQL이 옵티마이저 한계를 초과할 때
  • OLTP/OLAP 혼합 시나리오에서 Column Engine과 Row Engine 선택이 모호할 때

이때 CDS는 SQL WITH HINT(...) 구문을 직접 작성하는 대신, 어노테이션을 통해 선언적으로 옵티마이저에게 신호를 전달한다. 다음 표는 세 어노테이션의 역할을 비교한 것이다.

어노테이션대상 계층주요 효과
@AbapCatalog.bufferingABAP Application ServerTable Buffer 활성화/비활성화 및 모드 지정
@AbapCatalog.dbHintsHANA DB Layer특정 DB 엔진/JOIN 전략을 옵티마이저에 힌트로 전달
@ObjectModel.usageTypeApplication/OData/Analytic뷰의 사용 목적(트랜잭션/분석/구조)을 선언하여 호출 패턴 최적화

비유하자면, 어노테이션은 GPS 내비게이션에 "고속도로 우선" 같은 옵션을 켜는 것과 같다. 경로를 직접 그리는 것이 아니라, 옵티마이저(라우터)가 더 나은 선택을 하도록 우선순위를 알려주는 것이다.

3. 1단계: @AbapCatalog.buffering — 테이블 버퍼링 전략 선언

ABAP Application Server는 자주 조회되는 마스터성 데이터를 메모리에 캐시한다. CDS 뷰에서도 동일한 메커니즘을 사용할 수 있는데, 이때 사용하는 어노테이션이 @AbapCatalog.buffering이다. 주요 하위 속성은 다음과 같다.

  • status: #ACTIVE / #NOT_ALLOWED / #SWITCHED_OFF
  • type: #SINGLE / #GENERIC / #FULL
  • numberOfKeyFields: GENERIC 모드에서 첫 N개 키 필드 기준

다음은 국가 코드 마스터처럼 변경 빈도가 낮고 조회 빈도가 높은 데이터에 적용하는 예시이다.

@AbapCatalog.sqlViewName: 'ZV_COUNTRY_BUF'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.buffering.status: #ACTIVE
@AbapCatalog.buffering.type: #FULL
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Country Master (fully buffered)'
define view Z_C_Country
  as select from t005
{
  key land1 as Country,
      landx as CountryName,
      natio as Nationality,
      spras as Language
}

GENERIC 모드는 키 필드의 앞쪽 N개를 기준으로 부분 버퍼링한다. 예를 들어 BUKRS(회사 코드) 단위로 자주 조회되는 데이터라면 다음과 같이 선언한다.

@AbapCatalog.buffering.status: #ACTIVE
@AbapCatalog.buffering.type: #GENERIC
@AbapCatalog.buffering.numberOfKeyFields: 1
define view Z_C_CompanyConfig
  as select from t001
{
  key bukrs as CompanyCode,
  key ktopl as ChartOfAccounts,
      butxt as CompanyName
}

버퍼링을 적용해선 안 되는 경우도 분명히 존재한다. 변경 빈도가 분 단위인 트랜잭션 테이블, 대용량(수백만 행) 테이블, JOIN/집계가 포함된 뷰, 권한 체크가 행 단위로 일어나야 하는 뷰가 그 예다. 이런 경우 #NOT_ALLOWED를 명시해 다른 개발자가 실수로 활성화하지 못하게 막는 편이 안전하다.

4. 2단계: @AbapCatalog.dbHints — HANA 특정 힌트 쿼리 주입

@AbapCatalog.dbHints는 CDS 뷰가 HANA에서 실행될 때 SQL 끝에 WITH HINT(...) 절을 부착한다. 옵티마이저가 잘못된 plan을 선택할 때 임시 회피 수단으로 매우 유용하다. 단, 힌트는 옵티마이저 버전이 올라가면서 무의미해지거나 역효과를 낼 수 있으므로 주기적 재검증이 권장된다.

@AbapCatalog.sqlViewName: 'ZV_SALES_HINT'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.dbHints: [ { dbsystem: #HDB,
                         hint: 'USE_OLAP_PLAN' },
                       { dbsystem: #HDB,
                         hint: 'NO_CALC_VIEW_UNFOLDING' } ]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Aggregation with HANA hints'
define view Z_C_SalesAgg
  as select from vbak as h
  inner join vbap as i on h.vbeln = i.vbeln
{
  key h.vbeln as SalesDocument,
      h.kunnr as Customer,
      sum(i.netwr) as NetValueTotal,
      h.waerk as Currency
}
group by h.vbeln, h.kunnr, h.waerk

자주 사용되는 HANA 힌트 후보는 다음과 같다.

  • USE_OLAP_PLAN — 집계가 많은 분석 쿼리에서 OLAP 엔진을 강제
  • NO_CALC_VIEW_UNFOLDING — 칼큘레이션 뷰 펼침 방지로 plan 안정화
  • NO_INLINE — 서브쿼리 인라인 방지
  • RESULT_LAG('hana_sr', N) — 시스템 복제 환경에서 N초 이내 지연 허용

실무에서는 먼저 힌트 없이 plan을 측정한 뒤, 병목이 확인된 경우에 한해 최소한의 힌트만 적용하는 것이 일반적이다. dbHints를 적용한 뷰는 별도 주석(@EndUserText.label 등)에 적용 이유와 검증 일자를 남겨두면 후임자가 무의미해진 힌트를 제거할 때 도움이 된다.

5. 3단계: @ObjectModel.usageType — 뷰 용도 명시

@ObjectModel.usageType은 CDS 뷰가 어떤 시나리오에서 사용되는지를 SAP 프레임워크(예: SADL, Analytic Manager, OData)에 알리는 메타데이터다. 잘못 선언하면 OData 노출 자체가 거부되거나, BW Query Generator가 뷰를 인식하지 못한다. 주요 하위 속성은 세 가지다.

  • serviceQuality: #A(Analytical) / #T(Transactional) / #D(DataConsumption) / #S(Standard) / #X(Extension) / #B(Basic)
  • sizeCategory: #XS~#XXL — 예상 행 수 범주
  • dataClass: #MASTER / #TRANSACTIONAL / #ORGANIZATIONAL / #CUSTOMIZING / #MIXED

다음은 분석 큐브로 사용할 뷰의 일반적 선언이다.

@AbapCatalog.sqlViewName: 'ZV_SALES_CUBE'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@Analytics.dataCategory: #CUBE
@ObjectModel.usageType: { serviceQuality: #A,
                          sizeCategory:   #XL,
                          dataClass:      #TRANSACTIONAL }
@EndUserText.label: 'Sales Analytical Cube'
define view Z_C_SalesCube
  as select from Z_I_SalesItem
{
  @AnalyticsDetails.query.axis: #ROWS
  key SalesOrganization,
  @AnalyticsDetails.query.axis: #ROWS
  key DistributionChannel,
  @DefaultAggregation: #SUM
      NetAmount,
  @DefaultAggregation: #SUM
      Quantity
}

반대로 OData 트랜잭션 서비스로 노출할 뷰는 다음과 같이 선언한다.

@ObjectModel.usageType: { serviceQuality: #T,
                          sizeCategory:   #M,
                          dataClass:      #TRANSACTIONAL }
@ObjectModel.semanticKey: [ 'SalesOrder' ]
@OData.publish: true
define view Z_C_SalesOrderTP
  as select from Z_I_SalesOrder
{
  key SalesOrder,
      Customer,
      OrderDate,
      NetAmount,
      Currency
}

serviceQuality는 단순한 라벨이 아니라 프레임워크가 실제 호출 전략을 바꾸는 신호로 사용한다. 예를 들어 #T로 선언된 뷰는 SADL이 단건 fetch 패턴(키 기반 SELECT SINGLE)을 우선하고, #A 뷰는 BW Analytic Manager가 집계 푸시다운을 시도한다. sizeCategory가 작게 선언된 뷰에 페이징 없이 전체 조회 요청이 들어오면 프레임워크가 경고 또는 차단하는 경우도 있다.

6. HANA Plan Visualizer로 실행 계획 확인 및 힌트 효과 검증

힌트를 적용한 뒤 반드시 실행 계획을 비교해야 한다. ABAP에서 CDS 뷰의 plan을 확인하는 일반적 절차는 다음과 같다.

  1. SE11에서 CDS의 SQL 뷰 이름(예: ZV_SALES_HINT) 확인
  2. HANA Studio 또는 SAP HANA Database Explorer 접속
  3. SQL Console에서 해당 뷰에 대해 EXPLAIN PLAN FOR SELECT ... 실행
  4. Plan Visualizer(PlanViz)로 실제 실행 후 노드별 실행 시간/카디널리티 확인
-- HANA SQL Console
EXPLAIN PLAN SET STATEMENT_NAME = 'SALES_HINT_TEST' FOR
  SELECT * FROM zv_sales_hint WHERE customer = '0000010001';

SELECT operator_name, operator_details, execution_engine, table_name
  FROM explain_plan_table
 WHERE statement_name = 'SALES_HINT_TEST'
 ORDER BY operator_id;

PlanViz 그래프에서 다음 지표를 우선 확인한다.

  • Execution Engine: ROW / COLUMN / OLAP / ESX — usageType과 dbHints 의도대로 라우팅되었는지
  • Cardinality vs. Estimation: 추정과 실제 행 수 차이가 10배 이상이면 통계 갱신 필요
  • Filter Pushdown: 상위 뷰의 WHERE 조건이 하위 테이블까지 내려갔는지
  • Network Time: scale-out HANA에서 노드 간 데이터 전송 비용

ABAP 측에서는 ST05(Performance Trace)SAT(Runtime Analysis)로 OPEN SQL과 CDS 호출 시간을 분리해서 측정하고, SQLM(SQL Monitor)으로 운영 환경의 실제 호출 빈도를 확인할 수 있다. 트랜잭션 코드 RSDDB_LOGGING_HANA 등을 통해 옵티마이저 통계 수동 갱신도 검토 대상이다.

7. 자주 겪는 함정 — 힌트 충돌, 전송 후 캐시 무효화, usageType 잘못 설정

Q1. dbHints가 적용된 뷰를 다른 뷰가 참조하면 힌트가 전파되는가?

일반적으로 힌트는 해당 뷰의 SQL 생성 시점에만 부착되며, 상위 뷰에 자동 전파되지 않는다. 중첩 뷰에서 동일한 힌트가 필요하면 각 레벨에 명시해야 한다. 또한 상위 뷰가 다른 힌트를 부착하면 충돌로 인해 옵티마이저가 두 힌트 모두 무시할 수 있다.

Q2. buffering을 ACTIVE로 변경했는데 즉시 반영되지 않는다.

버퍼링 메타데이터는 ABAP 인스턴스의 nametab에 캐시된다. 트랜스포트 후에도 $TAB(SE38: RSDBBUFR) 또는 /$SYNC로 버퍼를 무효화하지 않으면 변경 사항이 반영되지 않는 경우가 있다. 또한 INSERT/UPDATE가 일어나는 테이블에 FULL 버퍼링을 켜면 모든 work process에 변경 동기화 부하가 발생하므로, 변경 빈도가 분 단위 이상이면 다시 검토해야 한다.

Q3. usageType.serviceQuality #A로 선언했더니 OData 노출이 실패한다.

Analytical 뷰는 차원/측정 어노테이션(@DefaultAggregation, @AnalyticsDetails)이 정합적으로 채워져야 한다. 또한 @Analytics.dataCategory#CUBE#DIMENSION 중 하나로 선언되어야 BW Query Generator가 인식한다. 트랜잭션 뷰처럼 사용할 거라면 #T로 바꿔야 한다.

Q4. 운영에서는 빠른데 개발 시스템에서만 느리다.

HANA 옵티마이저는 통계와 데이터 양에 민감하다. 운영과 동일한 plan이 보장되지 않으므로 PlanViz 결과를 운영 기준으로 판단하고, 개발 시스템 plan만으로 힌트를 추가하지 않는 것이 안전하다. 통계 누락 의심 시 UPDATE STATISTICS 또는 데이터 샘플링 옵션을 점검한다.

Q5. 힌트를 너무 많이 추가했더니 옵티마이저가 더 나빠졌다.

힌트는 옵티마이저의 자유도를 제약한다. HANA 버전이 올라가면서 자동 최적화가 더 똑똑해지는 경우가 많으므로, 분기마다 힌트 제거 후 plan을 비교하는 회귀 테스트가 권장된다.

8. 응용 패턴 — 대용량 조회 최적화, Analytical 뷰 집계 성능 개선

실무에서 위 세 어노테이션을 결합해 사용하는 대표 패턴을 정리한다.

패턴 A: 마스터 데이터 + 트랜잭션 데이터 조인 뷰 — 마스터는 별도 버퍼링 뷰로 분리하고, 조인 뷰에서는 버퍼링을 끄되 usageType.sizeCategory를 정확히 선언한다.

@AbapCatalog.sqlViewName: 'ZV_ORDER_ENR'
@AbapCatalog.buffering.status: #NOT_ALLOWED
@ObjectModel.usageType: { serviceQuality: #D,
                          sizeCategory:   #L,
                          dataClass:      #MIXED }
@AccessControl.authorizationCheck: #CHECK
define view Z_C_OrderEnriched
  as select from Z_I_SalesOrder as o
  left outer join Z_C_Country as c
    on  o.ShipToCountry = c.Country
{
  key o.SalesOrder,
      o.Customer,
      c.CountryName,
      o.NetAmount,
      o.Currency
}

패턴 B: 대용량 집계 큐브 + OLAP 힌트 — 분석 큐브에는 USE_OLAP_PLAN을 부착해 칼큘레이션 엔진 경로를 안정화한다.

@AbapCatalog.sqlViewName: 'ZV_FIN_CUBE'
@AbapCatalog.dbHints: [ { dbsystem: #HDB, hint: 'USE_OLAP_PLAN' } ]
@Analytics.dataCategory: #CUBE
@ObjectModel.usageType: { serviceQuality: #A,
                          sizeCategory:   #XXL,
                          dataClass:      #TRANSACTIONAL }
@AccessControl.authorizationCheck: #CHECK
define view Z_C_FinancialCube
  as select from Z_I_FiDocumentItem
{
  key CompanyCode,
  key FiscalYear,
  key FiscalPeriod,
      @DefaultAggregation: #SUM
      AmountInCompanyCodeCurrency,
      @DefaultAggregation: #SUM
      AmountInTransactionCurrency
}

패턴 C: 트랜잭션 OData 서비스 — 단건 fetch 패턴에 최적화하기 위해 sizeCategory를 작게, serviceQuality를 #T로 선언하고 키 기반 필터가 항상 들어오도록 OData 메타데이터에 강제한다. 이 경우 dbHints는 추가하지 않고 옵티마이저 기본 plan을 신뢰하는 것이 일반적이다.

마지막으로, 어떤 패턴이든 측정 없이는 최적화도 없다는 원칙이 적용된다. ST05/SAT/SQLM/PlanViz 네 가지 도구로 적용 전후를 비교 측정하고, 결과를 git 또는 변경 요청서에 기록해 두면 후속 유지보수가 훨씬 수월해진다. CDS 어노테이션은 강력하지만, 그 자체가 마법이 아니라 옵티마이저와의 협업 도구라는 점을 기억하자.

댓글 0

아직 댓글이 없습니다.