따옴표 하나가 만든 야근, 타입드 리터럴이 막아준다
금요일 오후 6시, 운영 환경에서 갑작스럽게 떨어진 단가 계산 잡(Job)이 멈춰 섰다. 디버거를 열어보니 원인은 어이없게도 한 줄의 코드였다. DATA(lv_quantity) = '00100'. 작성자는 정수 100을 의도했지만 ABAP은 이를 길이 5의 문자 리터럴로 해석했고, 이후 산술 연산에서 암묵적 변환을 거치며 소수점 자릿수가 어긋났다. 이 한 줄을 DATA(lv_quantity) = CONV i( '100' ). 또는 더 명확하게 int4\`100\` 같은 타입드 리터럴(Typed Literal)로 작성했더라면, 컴파일 시점에 의도가 명확해지고 런타임 캐스팅 비용과 데이터 손실 위험을 동시에 줄일 수 있었다. 이 글은 ABAP 7.55 이후 정식 도입된 타입드 리터럴이 왜 단순한 문법 설탕이 아니라 코드 안전성을 끌어올리는 도구인지, 그리고 CDS/AMDP/RAP 환경에서 어떻게 활용해야 하는지를 정리한다.
왜 타입드 리터럴이 등장했는가
전통적인 ABAP에서 모든 리터럴은 사실상 두 가지 얼굴을 가졌다. 작은따옴표로 감싼 텍스트 필드 리터럴('ABC'), 백틱으로 감싼 스트링 리터럴(\`ABC\`), 그리고 부호 없는 정수 리터럴(42)이다. 정수가 아닌 모든 숫자, 즉 패킹 십진수나 부동소수점은 별도의 리터럴 표기가 없어서 문자 리터럴로 쓴 뒤 컨텍스트에 따라 암묵 변환되도록 맡겨야 했다. 예를 들어 DATA pi TYPE p LENGTH 8 DECIMALS 5 VALUE '3.14159'. 처럼 쓰는 방식이다. 이 방식은 두 가지 문제가 있다. 첫째, 코드를 읽는 사람이 해당 리터럴의 실제 타입을 즉시 알 수 없다. 둘째, 컨텍스트가 모호한 인라인 표현식에서는 컴파일러가 가장 보수적인 타입으로 해석해 의도와 다른 결과를 낳을 수 있다.
타입드 리터럴은 이 모호함을 제거하기 위해 type_name\`value\` 형태로 타입을 앞에 명시하도록 한 표현식이다. ABAP Platform 2020(7.55) 릴리스에서 CDS DDIC 기반 뷰에 먼저 도입되었고, 이후 ABAP SQL과 일반 ABAP 표현식까지 확장되었다. SAP BTP ABAP Environment와 S/4HANA 2021 이후 on-premise 모두에서 사용할 수 있으며, 클린 ABAP 가이드라인에서도 권장하는 패턴이다.
내부 동작 원리: 컴파일 타임에 결정되는 타입
타입드 리터럴은 단순히 표기만 바꾸는 것이 아니다. 컴파일러는 int4\`100\`을 만나는 즉시 해당 토큰의 데이터 타입을 빌트인 i(4바이트 정수)로 확정한다. 따라서 후속 산술 연산이나 비교 연산에서 별도의 캐스팅이 일어나지 않는다. 반면 '100'으로 쓰면 컨텍스트에 따라 c(3)로 잡힌 뒤 산술 연산 직전에 숫자로 변환되는 과정이 추가된다. 변환 자체는 빠르지만, 변환 실패 시 CX_SY_CONVERSION_NO_NUMBER 같은 예외가 런타임에 터지므로 디버깅 비용이 크다.
지원되는 빌트인 타입에는 int1, int2, int4, int8, d, t, utclong, decfloat16, decfloat34, string, xstring, numc, char, raw 등이 있다. 일부 타입(char, numc, raw)은 길이까지 명시하는 형태로 사용한다. 한 가지 주의할 점은 ABAP SQL 컨텍스트와 일반 ABAP 컨텍스트가 허용하는 타입 집합이 약간 다르다는 것이다. 예컨대 p(packed) 타입은 일반 ABAP 표현식에서는 타입드 리터럴로 직접 표기할 수 없고, ABAP SQL의 캐스트 연산자나 CONV를 거쳐야 한다.
실전 예제 1: 가격 계산 로직에서 의도 명확히 하기
전형적인 시나리오부터 시작해보자. 할인율을 곱해 최종 가격을 산출하는 메서드다. 기존 스타일과 타입드 리터럴 스타일을 비교한다.
" 기존 방식: 리터럴 타입이 모호함
METHOD calc_final_price_old.
DATA: lv_base_price TYPE p LENGTH 9 DECIMALS 2,
lv_discount TYPE p LENGTH 5 DECIMALS 4,
lv_result TYPE p LENGTH 9 DECIMALS 2.
lv_base_price = '12500.00'.
lv_discount = '0.0850'.
lv_result = lv_base_price * ( 1 - lv_discount ).
rv_price = lv_result.
ENDMETHOD.
" 개선된 방식: 타입드 리터럴 + CONV로 의도 표현
METHOD calc_final_price_new.
DATA(lv_base_price) = CONV decfloat34( \`12500.00\` ).
DATA(lv_discount) = CONV decfloat34( \`0.0850\` ).
DATA(lv_one) = decfloat34\`1.0\`.
DATA(lv_result) = lv_base_price * ( lv_one - lv_discount ).
rv_price = CONV #( lv_result ).
ENDMETHOD.
개선된 버전에서 주목할 점은 두 가지다. 첫째, decfloat34\`1.0\`처럼 상수도 타입드 리터럴로 표기해 곱셈에 참여하는 모든 피연산자의 타입이 통일된다. 둘째, CONV 연산자와 결합해 입력값이 문자열 출처(예: 외부 JSON 페이로드)임을 명시적으로 드러낸다. 코드 리뷰어가 한눈에 의도를 파악할 수 있다.
실전 예제 2: ABAP SQL에서 WHERE 절 안정성 확보
타입드 리터럴이 가장 빛나는 곳 중 하나가 ABAP SQL이다. 특히 WHERE 절에서 호스트 변수 대신 리터럴을 직접 쓸 때 타입 불일치로 인한 인덱스 미사용 문제가 자주 발생한다.
" 주문 상태 코드가 numc(2) 컬럼인데 문자 리터럴로 비교
SELECT order_id, customer_id, total_amount
FROM zorder_header
WHERE status_code = '03'
AND order_date >= '20260101'
INTO TABLE @DATA(lt_orders).
" 타입드 리터럴로 컬럼 타입과 정확히 일치시키기
SELECT order_id, customer_id, total_amount
FROM zorder_header
WHERE status_code = numc2\`03\`
AND order_date >= d\`20260101\`
INTO TABLE @DATA(lt_orders_safe).
두 번째 쿼리는 컬럼 정의와 리터럴 타입이 일치하므로 옵티마이저가 변환 함수를 끼워 넣을 필요가 없다. HANA DB에서 컬럼에 함수를 씌우면 인덱스를 못 타는 경우가 종종 있는데, 타입드 리터럴은 이런 함정을 사전에 차단한다. 또한 코드 리뷰 시 status_code 컬럼이 numc(2)임을 SELECT 문만 보고도 추론할 수 있다는 부수 효과도 있다.
CDS 뷰 엔티티에서 가상 컬럼을 정의할 때도 동일한 원칙이 적용된다.
@AccessControl.authorizationCheck: #CHECK
define view entity ZC_OrderSummary
as select from zorder_header as oh
{
key oh.order_id,
oh.customer_id,
oh.total_amount,
case oh.status_code
when numc2\`01\` then char10\`NEW\`
when numc2\`02\` then char10\`CONFIRMED\`
when numc2\`03\` then char10\`SHIPPED\`
else char10\`UNKNOWN\`
end as status_label,
cast( decfloat34\`0.10\` as abap.dec(5,4) ) as default_tax_rate
}
CDS 뷰 엔티티에서 case 표현식의 각 분기는 동일한 타입이어야 한다. 타입드 리터럴이 없던 시절에는 cast를 매 분기마다 붙여야 했지만, 이제는 리터럴 자체가 타입을 알고 있어 코드가 훨씬 깔끔해진다.
실전 예제 3: RAP 비헤이비어 구현에서의 활용
RAP(RESTful ABAP Programming) 모델의 비헤이비어 풀에서 액션이나 검증 로직을 구현할 때, 상수성 메시지 번호나 코드 값이 자주 등장한다. 이 부분도 타입드 리터럴로 통일하면 가독성과 안전성이 동시에 개선된다.
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS validate_quantity FOR VALIDATE ON SAVE
IMPORTING keys FOR Order~validateQuantity.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD validate_quantity.
READ ENTITIES OF z_i_order IN LOCAL MODE
ENTITY Order
FIELDS ( order_id quantity unit )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
IF ls_order-quantity < int4\`1\`.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message(
id = char20\`ZORDER_MSG\`
number = numc3\`015\`
severity = if_abap_behv_message=>severity-error
v1 = ls_order-order_id )
%element-quantity = if_abap_behv=>mk-on
) TO failed-order.
ENDIF.
IF ls_order-unit = char3\`PC \` AND
ls_order-quantity <> trunc( ls_order-quantity, int4\`0\` ).
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message(
id = char20\`ZORDER_MSG\`
number = numc3\`016\`
severity = if_abap_behv_message=>severity-error )
) TO reported-order.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
위 예제에서 numc3\`015\`처럼 메시지 번호를 타입드 리터럴로 표기하면, T100 메시지 클래스의 키 타입(NUMC3)과 정확히 일치하게 된다. char3\`PC \`처럼 길이까지 명시한 char 리터럴은 trailing space까지 보존되므로 단위 코드 같은 고정폭 필드 비교에 적합하다.
고급 패턴: 상수 클래스와의 결합, 그리고 한계
실무에서는 매직 넘버를 흩뿌리기보다 상수 인터페이스로 모아두는 패턴이 일반적이다. 이때 상수 정의에 타입드 리터럴을 활용하면 의도가 한층 명확해진다.
INTERFACE zif_order_constants.
CONSTANTS:
BEGIN OF status,
new TYPE numc2 VALUE numc2\`01\`,
confirmed TYPE numc2 VALUE numc2\`02\`,
shipped TYPE numc2 VALUE numc2\`03\`,
closed TYPE numc2 VALUE numc2\`99\`,
END OF status,
min_quantity TYPE i VALUE int4\`1\`,
default_tax TYPE decfloat34 VALUE decfloat34\`0.10\`,
currency_krw TYPE waers VALUE char5\`KRW \`.
ENDINTERFACE.
다만 모든 상황에서 만능은 아니다. 알아둘 한계가 몇 가지 있다.
- 릴리스 의존성: 일반 ABAP에서 타입드 리터럴을 전면 사용하려면 ABAP Platform 2021(7.56) 이상이 권장된다. 그 이전 릴리스에서는 CDS와 ABAP SQL 일부 컨텍스트에서만 지원되므로, 다운포팅이 필요한 코드라면 사용 범위를 확인해야 한다.
- p 타입의 부재: 패킹 십진수
p는 길이/소수점 조합이 너무 다양해 직접 리터럴 표기를 지원하지 않는다.decfloat16이나decfloat34를 거쳐CONV로 변환하는 우회가 일반적이다. - 가독성 과잉: 모든 정수 1을
int4\`1\`로 쓰는 건 오히려 노이즈가 된다. 컨텍스트가 명확한 부호 없는 정수 리터럴은 그대로 두고, 타입 모호성이 생길 수 있는 곳에 선별적으로 적용하는 것이 좋다. - 유니코드 길이 함정:
char타입드 리터럴의 길이는 문자 수 기준이지만, 데이터베이스 컬럼이 바이트 기준인 경우 길이 계산이 어긋날 수 있다. 멀티바이트 문자를 다룰 때는 별도 검증이 필요하다.
자주 묻는 질문 세 가지
Q1. VALUE #( ... ) 안에서도 타입드 리터럴을 쓸 수 있나? 가능하다. 다만 내부 테이블 행 구조의 컴포넌트 타입과 타입드 리터럴 타입이 일치하지 않으면 컴파일 오류가 난다. 이 점이 오히려 장점인데, 무심코 '01'로 적어 통과되던 코드가 명시적 오류로 잡히기 때문이다.
Q2. ABAP Unit 테스트에서 기대값을 어떻게 적어야 하나? 기대값은 가능한 한 타입드 리터럴로 적자. cl_abap_unit_assert=>assert_equals( exp = int4\`100\` act = lv_result )처럼 쓰면 비교 양변의 타입이 명확해져, 타입 불일치로 인한 어설션 실패를 빠르게 식별할 수 있다.
Q3. 성능 차이가 정말 있나? 단일 연산 수준에서는 미미하다. 그러나 루프 안에서 수십만 번 반복되는 비교나 SQL 옵티마이저 플랜 변화 같은 거시적 영향에서는 차이가 누적된다. 무엇보다 진짜 이득은 런타임 변환 예외를 컴파일 타임 오류로 끌어올린다는 점이다.
마무리하며
타입드 리터럴은 화려한 신기능은 아니다. 그러나 ABAP이라는 언어가 수십 년간 안고 있던 리터럴의 타입 모호성이라는 묵은 숙제를 해결하는 도구다. 클린 ABAP 가이드라인이 권장하듯, 새로 작성하는 코드부터 단계적으로 적용해보자. 특히 외부 데이터를 받아 계산하는 인터페이스 레이어, ABAP SQL의 WHERE 절, CDS의 case 표현식, RAP 비헤이비어의 메시지 코드처럼 타입 일치가 중요한 지점에서부터 시작하면 효과를 빠르게 체감할 수 있다.
댓글 0
아직 댓글이 없습니다.