ABAP

CDS 내장 함수 30초 정리 — SUBSTRING부터 CAST까지 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글에서 얻어갈 것

ABAP CDS(Core Data Services)는 단순한 SELECT 래퍼가 아니라, HANA Calculation Engine에서 직접 실행되는 강력한 데이터 모델링 레이어입니다. 그 중에서도 Built-in Functions는 ABAP 레이어로 데이터를 가져오기 전 DB 단에서 변환/계산을 끝낼 수 있게 해주는 핵심 도구입니다. 이 글에서는 CDS View Entity 안에서 자주 쓰이는 문자열, 타입 변환, 날짜/숫자 함수 세 묶음을 실무 시나리오로 풀어봅니다.

  • 문자열 함수로 고객명/상품코드 정제하기
  • CAST와 COALESCE로 NULL과 타입 불일치 안전하게 처리
  • DATS_DAYS_BETWEEN 등 날짜 함수로 영업일 기반 KPI 계산
  • 흔한 실수와 성능 고려사항까지 체크

먼저 알아두면 좋은 배경

이 글은 CDS View Entity(DEFINE VIEW ENTITY) 문법과 ABAP Development Tools(ADT) 사용 경험을 전제로 합니다. SELECT, JOIN, association 정의를 직접 작성해본 정도라면 충분합니다. SQL의 기본 함수 개념(WHERE, GROUP BY, NULL 의미)을 알고 있으면 Built-in Function의 동작 방식을 더 빠르게 익힐 수 있습니다.

환경, 버전, 준비물

예제는 다음 환경에서 동작을 확인하는 것을 권장합니다.

  • SAP S/4HANA 2022 이상 또는 SAP BTP ABAP Environment(Steampunk) 최신 릴리스
  • ABAP Platform 7.57 이상 (CDS View Entity 지원)
  • ABAP Development Tools(Eclipse) 3.30 이상
  • HANA DB 2.0 SPS06 이상 권장
  • 샘플 테이블: ZCUSTOMER(고객 마스터), ZSALESORDER(주문 헤더), ZPRODUCT(상품 마스터) — 직접 만들거나 EPM 데모 데이터(SNWD_*)로 대체 가능

구버전 CDS DDIC-based View(DEFINE VIEW)에서도 대부분 동일하게 동작하지만, 일부 함수는 View Entity에서만 정상 동작하거나 시맨틱이 약간 다를 수 있어 가능하면 View Entity로 작성하는 것이 일반적으로 권장됩니다.

핵심 개념: CDS Built-in Function이란

CDS Built-in Function은 CDS View 정의의 SELECT 리스트, WHERE 절, CASE 표현식 등에서 호출 가능한 내장 스칼라 함수입니다. ABAP 런타임이 아니라 데이터베이스(HANA)에서 푸시다운으로 실행되기 때문에, "ABAP에서 LOOP 돌면서 가공하던 로직"을 그대로 DB로 내려보낼 수 있다는 점이 가장 큰 가치입니다.

비유하자면, ABAP 코드가 식당 홀에서 손님 앞에서 음식을 다시 데우는 것이라면, CDS Built-in Function은 주방에서 미리 적정 온도로 내보내주는 것과 같습니다. 네트워크/메모리 비용이 크게 줄어듭니다.

크게 다음 카테고리로 나뉩니다.

  • 문자열: SUBSTRING, LEFT, RIGHT, UPPER, LOWER, CONCAT, LENGTH, LPAD, REPLACE
  • 타입 변환/조건: CAST, COALESCE, CASE WHEN 표현식
  • 날짜/시간: DATS_DAYS_BETWEEN, DATS_ADD_DAYS, DATS_ADD_MONTHS, DATS_IS_VALID, TSTMP_*
  • 숫자/수치: ABS, FLOOR, CEIL(또는 CEILING), ROUND, DIV, MOD, DIVISION

중요한 제약은 두 가지입니다. 첫째, 함수 결과는 정의된 반환 타입으로 고정되므로 컬럼 별칭(as)과 함께 명시적으로 다뤄야 합니다. 둘째, NULL 처리는 ABAP 초기값과 다르게 동작하므로 COALESCECASE로 방어 코드를 함께 두는 습관이 필요합니다.

1단계 예제: 문자열 함수로 고객 데이터 정제

먼저 가장 단순한 시나리오입니다. 고객 마스터(ZCUSTOMER)에서 이름을 표시용으로 정제하고, 사업자번호 앞 3자리만 노출하는 View를 만들어 보겠습니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Customer Display - String Functions'
define view entity ZC_CUSTOMER_DISPLAY
  as select from zcustomer as cust
{
  key cust.customer_id                          as CustomerId,

      // 1) 이름 전체를 대문자로 (검색 인덱싱 용)
      upper( cust.full_name )                   as FullNameUpper,

      // 2) 이름 길이 (UI 표시 길이 제한 체크)
      length( cust.full_name )                  as NameLength,

      // 3) 성(姓) 추출 — 앞 1글자만
      left( cust.full_name, 1 )                 as InitialChar,

      // 4) 사업자번호 앞 3자리 + 마스킹
      concat(
        substring( cust.biz_reg_no, 1, 3 ),
        '-****-****'
      )                                         as MaskedBizNo,

      // 5) 도메인만 추출하기 위한 substring (이메일 @ 뒤)
      substring( cust.email,
                 length( cust.email ) - 10,
                 10 )                           as EmailTail,

      cust.country
}
where cust.is_active = 'X'

핵심 포인트는 다음과 같습니다.

  • upper/lower는 모든 문자형 입력을 받으며, 결과 타입은 입력과 동일
  • substring(string, position, length)에서 position은 1부터 시작합니다(ABAP과 동일, JavaScript와 다름)
  • concat은 인자 2개만 받습니다. 3개 이상 이어붙이려면 concat(concat(a,b),c)처럼 중첩하거나 concat_with_space 등의 변형 함수를 사용
  • length는 문자형의 실제 문자 수를 INT4로 반환

2단계 예제: 타입 변환, NULL 안전 처리, 로깅용 분기

이번에는 주문 헤더(ZSALESORDER)와 고객을 조인하면서, NULL/누락 데이터를 안전하게 다듬고 분류 코드를 추가합니다. 실무에서 가장 자주 마주치는 패턴입니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Enriched - Cast / Coalesce / Case'
define view entity ZC_SALESORDER_ENRICHED
  as select from zsalesorder as so
    left outer join zcustomer as cust
      on so.customer_id = cust.customer_id
{
  key so.order_id                                  as OrderId,

      // 1) 고객명이 NULL(외부 시스템 미연계)인 경우 대체 텍스트
      coalesce( cust.full_name, 'UNKNOWN CUSTOMER' ) as CustomerName,

      // 2) 외부에서 들어온 문자형 금액을 DEC(15,2)로 안전 변환
      cast( so.amount_raw as abap.dec(15,2) )       as OrderAmount,

      // 3) 수량 NULL → 0 으로 보정 후 합산 가능하도록
      coalesce( so.quantity, cast( 0 as abap.int4 ) ) as OrderQty,

      // 4) 금액 구간 분류 (대시보드용 등급)
      case
        when cast( so.amount_raw as abap.dec(15,2) ) >= 10000 then 'A'
        when cast( so.amount_raw as abap.dec(15,2) ) >=  1000 then 'B'
        when cast( so.amount_raw as abap.dec(15,2) ) >=   100 then 'C'
        else 'D'
      end                                            as AmountGrade,

      // 5) 상태 코드 한글화 (UI 표시용)
      case so.status
        when '01' then 'CREATED'
        when '02' then 'CONFIRMED'
        when '03' then 'SHIPPED'
        when '04' then 'CLOSED'
        else 'UNKNOWN'
      end                                            as StatusText,

      so.order_date                                  as OrderDate
}

여기서 자주 놓치는 부분은 CAST 타입 지정입니다. CDS에서는 ABAP Dictionary 타입을 abap.dec(15,2), abap.int4, abap.char(40)처럼 명시합니다. preserving type: 옵션을 같이 쓰면 단위/통화 의미 정보를 유지할 수 있습니다.

COALESCE는 첫 번째로 NULL이 아닌 값을 반환합니다. ABAP 코드에서 IF로 분기하던 로직을 한 줄로 줄여주는데, 인자 타입이 서로 다르면 컴파일 에러가 나므로 두 번째 인자도 명시적으로 같은 타입이 되도록 CAST를 함께 쓰는 습관이 좋습니다.

3단계 예제: 날짜/숫자 함수로 KPI 뷰 구성 (성능/안정성)

마지막은 실제 분석용 KPI View입니다. 주문일과 현재일 차이로 "주문 후 경과일", 배송 예정일 계산, 단가 반올림, 그리고 음수 보정까지 한 번에 처리합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order KPI - Date / Numeric Functions'
@Metadata.allowExtensions: true
define view entity ZC_SALESORDER_KPI
  with parameters
    p_reference_date : abap.dats
  as select from zsalesorder as so
{
  key so.order_id                                              as OrderId,
      so.customer_id                                           as CustomerId,
      so.order_date                                            as OrderDate,

      // 1) 기준일과 주문일 사이 경과일 (음수 방지 위해 ABS)
      abs(
        dats_days_between( so.order_date, :p_reference_date )
      )                                                        as DaysSinceOrder,

      // 2) 출고 예정일 = 주문일 + 3 영업일(단순화: +3일)
      dats_add_days(
        so.order_date,
        cast( 3 as abap.int4 ),
        'FAIL'
      )                                                        as PlannedShipDate,

      // 3) 단가 내림 (예산 보수적 계산)
      floor( so.unit_price )                                   as UnitPriceFloor,

      // 4) 단가 올림 (재고 발주용 안전 수량 계산)
      ceil( so.unit_price )                                    as UnitPriceCeil,

      // 5) 박스 단위 포장 시 남는 수량 (12개/박스)
      division( so.quantity, 12, 0 )                           as FullBoxes,
      mod( so.quantity, 12 )                                   as RemainderQty,

      // 6) 할인율(%)을 0~100 범위로 강제
      case
        when so.discount_pct < 0   then cast( 0   as abap.dec(5,2) )
        when so.discount_pct > 100 then cast( 100 as abap.dec(5,2) )
        else so.discount_pct
      end                                                      as DiscountPctSafe
}
where so.order_date is not initial

이 View의 실무 포인트는 다음과 같습니다.

  • dats_days_between(d1, d2)는 d2 - d1 결과를 INT4로 반환합니다. 순서가 바뀌면 음수가 나오므로 abs로 감싸거나 비교 순서를 표준화하는 것이 안전합니다.
  • dats_add_days(date, days, on_error)의 세 번째 인자는 잘못된 날짜 발생 시 동작을 지정합니다. 'FAIL'(예외), 'NULL'(NULL 반환), 'INITIAL'(초기값) 중 선택하며, 분석 View에서는 보통 'NULL'이 안전합니다.
  • division(operand1, operand2, decimals)는 0으로 나누기 발생 시 NULL을 반환하므로 operand1 / operand2 직접 표기보다 안정적입니다.
  • mod는 정수 나머지로, 포장/배치 잔여 수량 계산에 유용합니다.
  • 파라미터(with parameters)로 기준일을 받아 ABAP에서 SELECT ... FROM zc_salesorder_kpi( p_reference_date = ... ) 형태로 호출하면 캐시 및 가독성이 향상됩니다.

성능 측면에서는 동일한 CAST/표현식을 여러 번 반복하기보다, 하위 View에서 한 번 계산해 상위 View가 재사용하도록 분리하는 것이 일반적으로 권장됩니다. HANA 옵티마이저가 푸시다운을 잘 수행하지만, 같은 식의 반복은 가독성/유지보수에 부담이 됩니다.

흔한 실수와 트러블슈팅

Built-in Function을 처음 도입할 때 자주 마주치는 함정과 해결 방법을 FAQ로 정리합니다.

Q1. "Function not allowed in this context" 에러가 납니다.
A. 일부 함수는 SELECT 리스트에서만 허용되며, WHERE 절이나 ON 조건에서는 사용할 수 없습니다. 또한 DDIC-based View(DEFINE VIEW)와 View Entity(DEFINE VIEW ENTITY)에서 지원 범위가 다릅니다. 예를 들어 일부 currency_conversion, unit_conversion 함수는 View Entity 기준으로 최신 시맨틱을 따르므로 가급적 View Entity로 작성하는 것이 좋습니다.

Q2. COALESCE 결과로 NULL이 그대로 나옵니다.
A. 모든 인자가 NULL이면 결과도 NULL입니다. 두 번째 인자에 반드시 NULL이 아닌 리터럴(또는 CAST된 0/공백)을 두어야 합니다. 또한 LEFT OUTER JOIN 결과 컬럼은 NULL이 될 수 있지만, ABAP에서 읽으면 자동으로 초기값으로 변환되므로 ABAP 단에서 디버깅하면 NULL이 보이지 않습니다. SQL Console(ADT의 Data Preview)에서 검증하세요.

Q3. SUBSTRING의 position이 0이거나 음수면 어떻게 되나요?
A. CDS의 substring은 position이 1부터 시작합니다. 0 또는 음수일 때 동작은 정의되지 않거나 빈 문자열을 돌려줄 수 있어 권장되지 않습니다. 동적 위치 계산이 필요하면 사전에 case>= 1 범위를 강제하세요.

Q4. DATS_ADD_DAYS 결과가 갑자기 비어 있습니다.
A. 입력 날짜가 '00000000'이거나 비정상 값인 경우입니다. on_error 옵션을 'NULL'로 두고 where date is not initial 조건을 함께 걸어두면 안전합니다.

Q5. CAST가 컴파일은 되는데 런타임에서 dump가 납니다.
A. 문자열 → 숫자 변환 시 공백, 콤마, 통화 기호가 섞여 있으면 변환 실패가 발생합니다. 원본 데이터를 먼저 정제(REPLACE 등)하거나, case when으로 패턴이 맞는 행만 CAST 대상으로 좁히는 방어 로직이 필요합니다.

이어서 살펴볼 만한 주제

Built-in Function에 익숙해졌다면 다음 주제로 확장해 보세요.

  • CDS Table Function: ABAP-managed function으로 HANA SQLScript와 연계하기
  • Currency/Unit Conversion 함수(currency_conversion, unit_conversion)로 다국적 매출 통일
  • Aggregation 함수 + GROUP BY로 분석 View 만들기
  • CDS Analytical View와 KPI 모델링(@Analytics.dataCategory: #CUBE)
  • ABAP RAP에서 계산 필드를 Behavior Determination으로 이전하는 기준

더 깊이 읽어볼 자료

댓글 0

아직 댓글이 없습니다.