📖 개요: 이 글에서 다루는 것
ABAP CDS 뷰만으로는 표현하기 어려운 복잡한 계산 로직을 만나면, CDS Table Function이 강력한 탈출구가 됩니다. 이 글은 가상의 물류 시나리오(창고 재고 회전율 분석)를 바탕으로 Table Function과 AMDP를 결합해 HANA 네이티브 성능을 끌어내는 실전 패턴을 3단계로 정리합니다.
- CDS 뷰 vs Table Function의 역할 분담 이해
- AMDP(SQLScript) 구현 메서드 작성과 클라이언트 처리
- 0으로 나누기 방어, 예외 처리, 호출 측 로깅 패턴
- 파라미터 푸시다운·DCL 접근 제어·테스트 더블 적용
📚 미리 알아두면 좋은 배경
CDS 뷰 엔티티의 기본 문법(select, association, 파라미터), ABAP 클래스 정의와 인터페이스 구현 경험, 기초 SQL(GROUP BY, JOIN, 집계 함수)을 알고 있다면 무리 없이 따라올 수 있습니다. SQLScript를 처음 접해도 괜찮습니다. 이 글의 예제는 표준 SQL과 거의 같은 형태로 시작해 점진적으로 확장합니다.
🔧 환경과 버전, 준비물
CDS Table Function은 SAP HANA 전용 기능입니다. 일반적으로 다음 환경이 필요합니다.
- 온프레미스: AS ABAP 7.50 이상 + HANA DB (S/4HANA 1610 이후 모든 릴리스 포함)
- 클라우드: SAP BTP ABAP Environment 또는 S/4HANA Cloud(ABAP Cloud 개발 모델, 릴리스된 오브젝트 범위 내 사용)
- 개발 도구: ADT(ABAP Development Tools for Eclipse) — Table Function DDL은 SE11/SE80에서 편집할 수 없습니다
예제에서 사용할 커스텀 오브젝트는 모두 창작 시나리오입니다. 재고 이동 이력 테이블 ZLGX_MOVEHIST(창고 ID, 품목 ID, 이동유형, 수량, 이동일자), 일별 재고 스냅샷 테이블 ZLGX_DAYSTOCK을 가정합니다.
💡 핵심 개념: Table Function은 어떻게 동작하는가
일반 CDS 뷰가 "레시피 카드를 건네주는 선언형 조립"이라면, Table Function은 "주방(HANA) 안에서 셰프가 직접 조리하는 방식"입니다. 뷰는 SELECT 문 하나로 결과 구조가 결정되지만, Table Function은 결과 구조(시그니처)만 CDS DDL로 선언하고 실제 로직은 AMDP 메서드의 SQLScript가 채웁니다.
동작 흐름을 도식화하면 이렇습니다.
ABAP SELECT 문 → CDS Table Function(시그니처) → AMDP 메서드(SQLScript) → HANA 실행 엔진 → 결과 반환
이 구조 덕분에 CDS 뷰 문법이 지원하지 않는 것들 — 윈도우 함수의 자유로운 조합, 계층 연산, 다단계 중간 테이블 변수, HANA 내장 함수 — 을 CDS 소비 계층(다른 뷰, ABAP SQL, OData)에 그대로 노출할 수 있습니다. 실무에서 자주 쓰이는 활용 사례는 일반적으로 세 갈래입니다.
- 복잡한 계산 로직: 회전율·가중평균·누적합처럼 여러 단계 집계가 필요한 지표 산출
- 성능 최적화: 애플리케이션 서버로 데이터를 끌어올리지 않고 DB 계층에서 코드 푸시다운
- CDS 뷰 스택 통합: Table Function 결과를 상위 CDS 뷰에서 조인해 분석 쿼리·Fiori 앱에 공급
주의할 점은 두 가지입니다. 첫째, HANA가 아닌 DB에서는 실행 자체가 불가능하므로 DB 이식성이 필요한 코드에는 부적합합니다. 둘째, 내부 로직이 옵티마이저에게 일부 블랙박스로 취급될 수 있어 필터 조건은 가급적 파라미터로 전달하는 것이 권장됩니다.
💻 실전 코드: 3단계로 완성하는 재고 회전율 분석
1단계 — 기본 구조: 시그니처 선언과 AMDP 구현
먼저 기간 파라미터를 받아 창고·품목별 출고량을 집계하는 Table Function을 만듭니다. ADT에서 DDL 소스를 생성합니다.
@EndUserText.label: '창고별 출고 집계'
@ClientHandling.type: #CLIENT_DEPENDENT
@ClientHandling.algorithm: #SESSION_VARIABLE
@AccessControl.authorizationCheck: #NOT_REQUIRED
define table function ZLGX_TF_TURNOVER
with parameters
p_date_from : abap.dats,
p_date_to : abap.dats
returns
{
client : abap.clnt;
warehouse_id : abap.char(4);
sku_id : abap.char(18);
total_out_qty : abap.quan(13,3);
move_count : abap.int4;
}
implemented by method
zcl_lgx_turnover=>get_turnover;
구현 클래스는 IF_AMDP_MARKER_HDB를 구현해야 하며, 메서드에 FOR TABLE FUNCTION을 지정합니다.
CLASS zcl_lgx_turnover DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_amdp_marker_hdb.
CLASS-METHODS get_turnover FOR TABLE FUNCTION zlgx_tf_turnover.
ENDCLASS.
CLASS zcl_lgx_turnover IMPLEMENTATION.
METHOD get_turnover BY DATABASE FUNCTION FOR HDB
LANGUAGE SQLSCRIPT
OPTIONS READ-ONLY
USING zlgx_movehist.
RETURN SELECT mandt AS client,
warehouse_id,
sku_id,
SUM( quantity ) AS total_out_qty,
COUNT( * ) AS move_count
FROM zlgx_movehist
WHERE mandt = SESSION_CONTEXT('CDS_CLIENT')
AND move_type = 'OUT'
AND move_date BETWEEN :p_date_from AND :p_date_to
GROUP BY mandt, warehouse_id, sku_id;
ENDMETHOD.
ENDCLASS.
핵심 포인트: SELECT 리스트의 컬럼 순서·타입이 DDL의 returns 목록과 정확히 일치해야 하고, 클라이언트 필터는 자동으로 걸리지 않으므로 SESSION_CONTEXT('CDS_CLIENT')로 직접 처리합니다.
2단계 — 실무 시나리오: 회전율 계산, 0 나누기 방어, 호출 측 예외 처리
회전율은 "출고량 ÷ 평균 재고"입니다. 평균 재고가 0인 품목이 반드시 존재하므로 NULLIF로 방어하고, 스냅샷 테이블과 조인합니다. AMDP 본문을 확장합니다.
RETURN
SELECT m.mandt AS client,
m.warehouse_id, m.sku_id,
SUM( m.quantity ) AS total_out_qty,
TO_DECIMAL( AVG( s.stock_qty ), 13, 3 ) AS avg_stock_qty,
TO_DECIMAL( SUM( m.quantity ) /
NULLIF( AVG( s.stock_qty ), 0 ), 10, 2 ) AS turnover_rate
FROM zlgx_movehist AS m
LEFT JOIN zlgx_daystock AS s
ON s.mandt = m.mandt
AND s.warehouse_id = m.warehouse_id
AND s.sku_id = m.sku_id
AND s.snap_date BETWEEN :p_date_from AND :p_date_to
WHERE m.mandt = SESSION_CONTEXT('CDS_CLIENT')
AND m.move_type = 'OUT'
AND m.move_date BETWEEN :p_date_from AND :p_date_to
GROUP BY m.mandt, m.warehouse_id, m.sku_id;
SQLScript 내부에서는 로깅이 제한적이므로, 운영 로그는 호출 측 ABAP에서 남기는 패턴이 일반적입니다.
TRY.
SELECT FROM zlgx_tf_turnover( p_date_from = @lv_from,
p_date_to = @lv_to )
FIELDS warehouse_id, sku_id, turnover_rate
WHERE turnover_rate < @lc_slow_threshold
ORDER BY turnover_rate ASCENDING
INTO TABLE @DATA(lt_slow_movers).
CATCH cx_amdp_execution_failed INTO DATA(lx_amdp).
" 애플리케이션 로그 기록 후 빈 결과로 폴백
log_error( lx_amdp->get_text( ) ).
CLEAR lt_slow_movers.
ENDTRY.
Table Function 실행 오류(잘못된 날짜 범위, 권한 문제 등)는 CX_AMDP_EXECUTION_FAILED 계열 예외로 전파되므로 반드시 TRY 블록으로 감싸는 것이 안전합니다.
3단계 — 프로덕션 대비: 푸시다운, 접근 제어, 테스트
성능 측면에서 가장 중요한 원칙은 필터를 파라미터로 밀어 넣는 것입니다. 외부 WHERE 절(예: 창고 ID)이 함수 내부까지 최적화되지 않을 수 있으므로, 자주 쓰는 필터는 p_warehouse 파라미터로 추가해 내부 WHERE에서 소비하도록 설계합니다. 또 윈도우 함수로 "창고별 상위 N 품목"을 DB 안에서 잘라내면 전송량이 크게 줄어듭니다.
lt_ranked = SELECT *,
RANK() OVER ( PARTITION BY warehouse_id
ORDER BY total_out_qty DESC ) AS out_rank
FROM :lt_aggregated;
RETURN SELECT client, warehouse_id, sku_id,
total_out_qty, avg_stock_qty, turnover_rate
FROM :lt_ranked WHERE out_rank <= :p_top_n;
보안은 DCL로 마무리합니다. @AccessControl.authorizationCheck: #CHECK로 바꾼 뒤 창고 권한 오브젝트와 매핑합니다.
@MappingRole: true
define role ZLGX_TURNOVER_DCL {
grant select on ZLGX_TF_TURNOVER
where ( warehouse_id ) = aspect pfcg_auth(
ZLGX_WH, ZLGX_WHID, ACTVT = '03' );
}
단위 테스트는 CDS Test Double Framework의 계층 테스트를 활용해, Table Function을 소비하는 상위 뷰를 검증할 때 함수 자체를 더블로 대체하는 방식이 일반적으로 권장됩니다(cl_cds_test_environment). AMDP 로직 자체는 ADT의 AMDP 디버거로 SQLScript 라인 단위 확인이 가능합니다.
⚠️ 흔한 실수와 트러블슈팅 FAQ
Q1. 활성화는 되는데 실행 시 "컬럼 개수/타입 불일치" 오류가 납니다.
DDL의 returns 목록과 SQLScript SELECT 리스트가 순서·개수·타입까지 일치해야 합니다. 특히 계산 컬럼은 TO_DECIMAL 등으로 명시적 형변환을 해두지 않으면 HANA가 추론한 타입과 어긋나기 쉽습니다. 활성화 시점이 아니라 첫 실행 시점에 발견되는 오류이므로 단위 테스트로 조기에 잡는 것이 좋습니다.
Q2. 다른 클라이언트 데이터까지 섞여 나옵니다.
Table Function 내부에서는 자동 클라이언트 처리(implicit client handling)가 적용되지 않습니다. 어노테이션(#CLIENT_DEPENDENT + #SESSION_VARIABLE) 선언과 함께 SQLScript WHERE에 SESSION_CONTEXT('CDS_CLIENT') 필터를 반드시 직접 넣어야 합니다.
Q3. 개발기는 HANA인데 일부 시스템에서 덤프가 발생합니다.
Table Function은 HANA 전용입니다. AnyDB 대응이 필요한 재사용 라이브러리라면 인터페이스 뒤에서 Table Function 경로와 일반 CDS 뷰 폴백 경로를 분기하는 설계를 검토하세요.
Q4. 외부 WHERE 조건을 걸어도 느립니다.
함수 내부는 옵티마이저에게 부분적 블랙박스입니다. 선택도 높은 필터(기간, 창고, 조직 단위)는 파라미터로 시그니처에 추가해 내부에서 소비하도록 리팩터링하는 것이 일반적으로 가장 효과적입니다.
🚀 이어서 살펴볼 주제
이번 예제를 확장하려면 (1) Table Function 결과를 소비하는 분석용 CDS 뷰 스택과 Analytical Query 어노테이션, (2) AMDP 프로시저(BY DATABASE PROCEDURE)와의 사용처 구분, (3) RAP 조회 전용 엔티티에 Table Function을 데이터 소스로 연결하는 패턴, (4) SQL Trace(ST05)와 PlanViz를 이용한 SQLScript 실행 계획 분석을 추천합니다. 특히 ABAP Cloud 환경에서의 릴리스 계약(released API) 제약도 함께 확인해 보세요.
댓글 0
아직 댓글이 없습니다.