개요 및 이 글에서 얻을 것
ABAP 7.40 이후 도입된 표현식 지향 프로그래밍(expression-oriented programming)은 코드를 간결하게 만들었지만, 한 줄에 여러 계산을 욱여넣다 보면 오히려 가독성이 떨어지는 역설이 생깁니다. VALUE, REDUCE, FILTER, COND 같은 구성자 표현식 내부에서 동일한 계산을 반복하거나, 임시 값을 담을 변수가 절실해질 때가 있죠. 이때 등장하는 것이 LET 표현식입니다. 이 글에서는 LET이 왜 필요한지, 어떻게 동작하는지, 그리고 실전에서 어떤 장면에 활용하면 코드 품질이 극적으로 개선되는지 단계별로 살펴봅니다.
- LET 표현식의 문법과 scope(유효 범위) 원리 이해
DATA()인라인 선언과의 본질적 차이 파악- VALUE/REDUCE/COND/FILTER와 결합한 실전 예제 작성
- 중복 계산 제거를 통한 성능 및 가독성 개선 패턴 습득
- 흔히 발생하는 scope 오류와 디버깅 방법 숙지
먼저 알아두면 좋은 것
이 글은 ABAP 7.40 SP08 이상의 신문법(constructor expression)에 익숙한 개발자를 대상으로 합니다. 최소한 VALUE #( ... )로 내부 테이블을 생성해본 경험과 FOR ... IN ... WHERE 형태의 테이블 컴프리헨션, 그리고 DATA(var) = ... 인라인 선언 문법은 사전에 알고 있어야 합니다. 또한 REDUCE의 INIT ... FOR ... NEXT 구조와 COND의 WHEN ... THEN 분기 표현식에 대한 기초 지식이 있으면 후반부 예제를 무리 없이 따라갈 수 있습니다.
실행 환경 및 준비 사항
LET 표현식은 ABAP 언어 버전 7.40 SP08부터 도입되었으며, 이후 7.50, 7.55, ABAP Cloud(Steampunk, BTP ABAP Environment)에서도 동일하게 지원됩니다. 다음과 같은 환경에서 동작을 확인할 수 있습니다.
- SAP NetWeaver AS ABAP 7.50 이상 또는 S/4HANA on-premise 2021/2022/2023
- SAP BTP ABAP Environment (Steampunk, 2024 릴리스 기준)
- ADT(ABAP Development Tools) for Eclipse 3.40 이상 권장 — 신문법 코드 어시스트와 quick-fix 지원
- 테스트 실행 환경:
CLASS ... DEFINITION FOR TESTING또는 단순 실행 클래스(IF_OO_ADT_CLASSRUN)
예제는 ABAP Cloud 호환 문법(cl_demo_output 대신 out->write 사용)을 기본으로 작성하되, 클래식 환경에서도 동일하게 동작하도록 표준 키워드만 사용합니다.
핵심 개념: LET은 표현식의 지역 변수다
전통적인 ABAP에서 중간 변수가 필요하면 DATA로 선언하고 값을 대입한 뒤 다음 줄에서 사용했습니다. 그러나 표현식 내부, 즉 한 줄짜리 식 안에서 중간 변수를 선언할 방법은 없었습니다. LET이 바로 이 공백을 메웁니다.
LET을 비유하자면 "표현식에 잠시 빌려주는 메모지"입니다. 표현식이 평가되는 동안에만 존재하고, 표현식이 끝나면 사라집니다. 함수형 언어의 let ... in ... 구문과 사실상 동일한 의미를 가지며, 다음과 같은 형태로 사용합니다.
... LET var1 = expr1
var2 = expr2
var3 = expr3 IN ...
중요한 동작 원리는 세 가지입니다.
- 읽기 전용(read-only): LET으로 선언된 변수는 한 번 할당되면 표현식 내부에서 값을 바꿀 수 없습니다. 이는 부작용(side effect)을 차단하여 표현식의 예측 가능성을 높입니다.
- 스코프 제한: 변수의 유효 범위는
IN뒤에 오는 표현식으로 한정됩니다. 표현식 바깥에서는 접근 불가하므로, 변수 이름 충돌이나 메모리 누수 걱정이 없습니다. - 순차적 가시성: 위에서 선언한 변수를 아래 LET 변수의 초기화 식에서 참조할 수 있습니다. 즉
var2 = var1 * 2같은 의존 계산이 가능합니다.
도식으로 표현하면 다음과 같습니다.
VALUE result_type(
LET tax_rate = '0.10'
base_price = compute_base( ... )
total = base_price * ( 1 + tax_rate )
IN
( amount = total currency = 'KRW' )
)
표현식이 끝나는 순간 세 개의 "메모지"는 메모리에서 사라집니다. 이는 DATA() 인라인 선언과 결정적으로 다른 점인데, DATA(x) = ...는 현재 프로시저(메서드/함수) 전체에서 유효한 변수를 만들기 때문입니다.
실전 코드 1단계: 기본 형태 익히기
먼저 가장 단순한 형태로 LET의 효과를 체감해봅시다. 주문 금액에 부가세를 더한 최종 청구 금액을 계산하는 시나리오입니다.
CLASS zcl_let_basic DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_let_basic IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
"LET 없이 작성한 버전 — 중복 계산이 두 번 발생
DATA(order_amount_old) = CONV decfloat34( '125000' ).
DATA(billing_old) = |기본: { order_amount_old }, | &&
|세금포함: { order_amount_old * '1.10' }|.
"LET을 사용한 버전 — 표현식 한 줄로 중간 변수 활용
DATA(billing_new) =
VALUE string(
LET base_amount = CONV decfloat34( '125000' )
vat_rate = CONV decfloat34( '0.10' )
final_total = base_amount * ( 1 + vat_rate )
IN |기본: { base_amount }, 세금포함: { final_total }| ).
out->write( billing_new ).
ENDMETHOD.
ENDCLASS.
LET 버전은 base_amount를 두 번 작성하지 않으면서도 final_total이 어떻게 계산되었는지 식 안에 명시적으로 드러납니다. 무엇보다 base_amount, vat_rate, final_total은 표현식 밖에서는 존재하지 않으므로 메서드 전체 네임스페이스를 오염시키지 않습니다.
실전 코드 2단계: VALUE/COND와 결합한 실무 시나리오
실무에서는 주문 데이터를 가공해 새로운 구조의 테이블로 변환하는 일이 잦습니다. 각 행마다 할인율을 조건에 따라 결정하고, 그에 따른 최종 가격을 산출하는 예제를 봅시다. 에러 처리와 로깅도 함께 고려합니다.
TYPES: BEGIN OF ty_raw_order,
order_id TYPE string,
customer TYPE string,
quantity TYPE i,
unit_price TYPE decfloat34,
vip_flag TYPE abap_bool,
END OF ty_raw_order,
tt_raw_order TYPE STANDARD TABLE OF ty_raw_order WITH EMPTY KEY.
TYPES: BEGIN OF ty_priced_order,
order_id TYPE string,
gross_amount TYPE decfloat34,
discount_rate TYPE decfloat34,
net_amount TYPE decfloat34,
tier_label TYPE string,
END OF ty_priced_order,
tt_priced_order TYPE STANDARD TABLE OF ty_priced_order WITH EMPTY KEY.
METHOD enrich_orders.
DATA(raw_orders) = VALUE tt_raw_order(
( order_id = 'A001' customer = 'KOSMO' quantity = 5 unit_price = '12000' vip_flag = abap_false )
( order_id = 'A002' customer = 'NEXUS' quantity = 30 unit_price = '8500' vip_flag = abap_true )
( order_id = 'A003' customer = 'ORION' quantity = 12 unit_price = '15000' vip_flag = abap_false ) ).
TRY.
DATA(priced_orders) = VALUE tt_priced_order(
FOR <raw> IN raw_orders
LET gross = <raw>-quantity * <raw>-unit_price
discount = COND decfloat34(
WHEN <raw>-vip_flag = abap_true THEN '0.15'
WHEN <raw>-quantity >= 20 THEN '0.10'
WHEN gross >= 150000 THEN '0.05'
ELSE '0' )
tier = COND string(
WHEN discount = '0.15' THEN 'VIP'
WHEN discount = '0.10' THEN 'BULK'
WHEN discount = '0.05' THEN 'PREMIUM'
ELSE 'STANDARD' )
IN ( order_id = <raw>-order_id
gross_amount = gross
discount_rate = discount
net_amount = gross * ( 1 - discount )
tier_label = tier ) ).
CATCH cx_sy_arithmetic_error INTO DATA(arith_err).
out->write( |가격 계산 오류: { arith_err->get_text( ) }| ).
RAISE EXCEPTION TYPE zcx_pricing_failed EXPORTING previous = arith_err.
ENDTRY.
result = priced_orders.
ENDMETHOD.
이 코드에서 주목할 부분은 LET 변수 간의 순차적 의존입니다. gross가 먼저 계산되고, 그 값을 discount 결정 로직이 참조하며, 다시 discount가 tier 결정에 쓰입니다. 만약 LET 없이 작성했다면 같은 <raw>-quantity * <raw>-unit_price 계산이 discount 결정 조건과 최종 결과 행 양쪽에 등장해 중복 평가가 발생하고, 향후 가격 공식이 바뀔 때 두 군데를 수정해야 하는 유지보수 부담이 생깁니다.
실전 코드 3단계: REDUCE와 결합한 집계 + 단위 테스트
마지막 단계는 REDUCE를 사용한 집계 로직에 LET을 적용한 사례입니다. 누적 통계(평균, 표준편차의 부분합)를 단일 표현식으로 산출하면서, 단위 테스트로 검증까지 수행합니다.
TYPES: BEGIN OF ty_stats,
count TYPE i,
sum_val TYPE decfloat34,
sum_sq TYPE decfloat34,
avg_val TYPE decfloat34,
END OF ty_stats.
CLASS zcl_stats_engine DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
CLASS-METHODS compute_stats
IMPORTING measurements TYPE STANDARD TABLE
RETURNING VALUE(result) TYPE ty_stats.
ENDCLASS.
CLASS zcl_stats_engine IMPLEMENTATION.
METHOD compute_stats.
result = REDUCE ty_stats(
INIT acc = VALUE ty_stats( )
FOR <m> IN measurements
LET measured = CONV decfloat34( <m> )
new_cnt = acc-count + 1
new_sum = acc-sum_val + measured
new_sq = acc-sum_sq + measured * measured
IN NEXT acc = VALUE ty_stats(
count = new_cnt
sum_val = new_sum
sum_sq = new_sq
avg_val = new_sum / new_cnt ) ).
ENDMETHOD.
ENDCLASS.
"단위 테스트
CLASS ltcl_stats DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS verify_average FOR TESTING.
ENDCLASS.
CLASS ltcl_stats IMPLEMENTATION.
METHOD verify_average.
DATA(samples) = VALUE int4_table( ( 10 ) ( 20 ) ( 30 ) ( 40 ) ).
DATA(stats) = zcl_stats_engine=>compute_stats( samples ).
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '25' )
act = stats-avg_val
msg = '평균 계산 결과가 예상과 다름' ).
ENDMETHOD.
ENDCLASS.
프로덕션 관점에서 점검할 사항은 세 가지입니다. 첫째, 성능: LET 변수는 표현식이 끝나면 해제되므로 메모리 부담이 거의 없으며, 동일 식의 재계산을 줄이는 효과로 일반적으로 약간의 성능 이점이 있습니다. 둘째, 가독성: 변수에 의미 있는 이름(new_sum, measured)을 부여하여 함수형 계산식의 의도를 자기 문서화합니다. 셋째, 테스트 가능성: 부작용이 없는 순수 표현식이라 ABAP Unit으로 결정론적 검증이 쉽습니다.
흔히 마주치는 실수와 FAQ
FAQ 1. "LET 변수에 값을 다시 대입하려고 했더니 컴파일 오류가 납니다."
LET 변수는 본질적으로 불변(immutable)입니다. x = x + 1 같은 재할당은 불가능하며, 변경이 필요하면 REDUCE의 NEXT 절을 사용하거나 표현식 외부의 DATA 변수를 활용해야 합니다. 이는 의도된 제약이며 표현식의 예측 가능성을 보장하는 핵심입니다.
FAQ 2. "표현식 외부에서 LET 변수를 참조하려는데 'unknown name' 오류가 발생합니다."
LET 변수는 IN 뒤의 식에서만 살아 있습니다. 표현식 평가가 끝나면 즉시 사라지므로, 외부에서 참조하려면 표현식 전체의 결과를 DATA()로 받아둔 뒤 그 결과에서 추출해야 합니다. "LET = 표현식 내부 전용 메모지"라는 비유를 기억하세요.
FAQ 3. "LET을 너무 많이 쓰니 오히려 코드가 복잡해 보입니다."
일반적으로 한 LET 블록에 변수가 4~5개를 넘기면 메서드로 추출하는 편이 낫습니다. LET은 "한 표현식 내에서 중복 계산을 제거하고 의미 있는 이름을 부여한다"는 목적에 충실할 때 가장 빛납니다. 변수가 많아지면 별도의 private 메서드로 캡슐화하여 호출하는 쪽이 가독성과 단위 테스트 모두에 유리합니다.
ADT 디버거 팁: LET 변수는 표현식 평가 중에만 변수 뷰에 표시되며, 라인을 빠져나오면 사라집니다. 값을 확인하고 싶다면 표현식 전후로 breakpoint를 걸고 표현식 결과를 검사하거나, 일시적으로 LET을 DATA() 인라인 선언으로 분해해 디버깅한 뒤 다시 합치는 방식이 권장됩니다.
이어서 익혀볼 ABAP 패턴
LET을 자유롭게 다룰 수 있다면 다음 단계로 FOR ... GROUPS BY를 활용한 그룹 집계, FILTER 표현식으로 보조키 기반 고속 필터링, 그리고 RAP(RESTful ABAP Programming Model)의 BDEF 액션/디터미네이션 구현에서 표현식을 활용하는 패턴으로 확장하길 권합니다. 특히 CDS 뷰의 가상 요소(virtual element) 구현 클래스에서 LET은 복잡한 계산 로직을 한 줄로 정리하는 데 매우 유용합니다. 또한 ABAP Cleaner 도구를 사용하면 기존 코드의 중복 표현식을 LET 패턴으로 자동 리팩토링하는 기능도 활용할 수 있습니다.
댓글 0
아직 댓글이 없습니다.