개요 및 학습 포인트
ABAP 7.52부터 정식 지원되는 FOR ... THEN ... UNTIL 표현식은 단순한 반복 카운터를 넘어, 수학적 수열(arithmetic/geometric sequence)을 한 줄의 인라인 표현식으로 만들어내는 강력한 기능입니다. 전통적인 DO ... ENDDO 루프와 보조 작업 영역(work area)으로 작성하던 코드를 함수형 스타일로 압축할 수 있으며, VALUE #( )나 REDUCE #( )와 결합했을 때 진가를 발휘합니다. 이 글에서는 월별 분석 구간 생성, 회계 주차 시퀀스, 대용량 배치 슬라이싱 같은 실무 시나리오를 통해 패턴을 익히게 됩니다.
- FOR ... THEN ... UNTIL의 평가 순서와 내부 동작 원리 이해
- WHILE 조건과 UNTIL 조건의 차이, 음수/역방향 시퀀스 처리
- VALUE #, REDUCE #와 결합한 인라인 테이블 생성 패턴 체득
- 월/주/배치 슬라이스 등 실무 범위 생성 패턴 3가지 적용
- 성능 비교 및 안티패턴 회피 기준 정립
알고 있어야 할 배경 지식
이 글은 advanced 수준이므로 다음 사항에 익숙하다고 가정합니다. ABAP 7.40 이상의 인라인 선언(DATA(...)), 생성자 표현식(VALUE #, NEW #, CORRESPONDING #), 그리고 람다와 비슷한 표현식 컨텍스트의 개념. 또한 내부 테이블(internal table)의 STANDARD/SORTED/HASHED 종류와 READ TABLE ... INTO 대비 ... = itab[ ... ] 표 표현식의 차이를 알면 따라가기 수월합니다.
실행 환경과 준비물
실습 코드는 다음 환경 기준으로 검증되었습니다.
- ABAP 플랫폼: SAP NetWeaver AS ABAP 7.52 이상 또는 SAP BTP ABAP Environment(Steampunk), S/4HANA 1909 이상의 ABAP Cloud 컴파일러
- 개발 도구: ABAP Development Tools(ADT) for Eclipse 2023-06 이상 권장 (구문 색상화와 quick-fix 지원)
- 실행 방식: ADT에서
CLASS zcl_for_then_demo DEFINITION PUBLIC FINAL CREATE PUBLIC형태의 클래스를 만들고IF_OO_ADT_CLASSRUN을 구현해 F9로 실행 - 참고: 7.50까지는
FOR ... WHILE만 지원되었기에 7.52 미만 시스템에서는 컴파일 오류가 발생합니다. 시스템 릴리스를 먼저 확인하세요
본 글에서 다루는 표현식은 ABAP Cloud(릴리스 제약이 강한 환경)에서도 일반적으로 사용 가능합니다. 다만 특정 NetWeaver 패치 레벨에서는 인라인 표현식의 메모리 거동이 다를 수 있으니, 운영 적용 전 본인 시스템 패치 노트를 확인하는 것을 권장합니다.
핵심 개념 파헤치기
전통적인 DO ... ENDDO가 "명령형 반복"이라면, FOR ... THEN ... UNTIL은 "수열을 정의하는 표현식"에 가깝습니다. 구문의 일반형은 다음과 같습니다.
FOR <var> = <start> THEN <next_expr> UNTIL <stop_cond>
여기서 <var>는 반복 변수, <start>는 초기값, <next_expr>는 다음 값을 계산하는 식(반드시 <var>를 참조해야 함), <stop_cond>는 반복 종료 조건입니다. 평가 순서를 도식화하면 다음과 같습니다.
* 1. var := start
* 2. UNTIL 조건 평가 → true이면 즉시 종료(현재 var 사용 안 됨)
* 3. body(VALUE/REDUCE 내부) 실행
* 4. var := next_expr (var의 이전 값 사용)
* 5. 2단계로 복귀
이 흐름의 핵심은 UNTIL이 "선평가"라는 점입니다. C 언어의 do { } while ()와 달리, 첫 반복 전에 조건을 검사하므로 0회 실행도 가능합니다. WHILE 변형은 의미만 반대일 뿐(조건이 true인 동안 반복) 평가 시점은 동일합니다.
비유하자면, DO/ENDDO는 "셰프가 직접 냄비를 들고 한 그릇씩 떠 담는 방식"이고, FOR THEN UNTIL은 "레시피(점화식)를 명시하고 컨테이너가 알아서 채워지는 방식"입니다. 그래서 VALUE itab_type( FOR ... )나 REDUCE int( FOR ... )처럼 컨테이너 생성자 안에서 사용해야 의미가 살아납니다.
또 하나 중요한 특성은 next_expr이 반드시 var를 갱신하는 식이어야 한다는 점입니다. 단순 증가는 var + 1이지만, var * 2로 기하수열을, var + step으로 임의 간격 산술수열을, 심지어 var BIT-AND ... 같은 비트 연산도 가능합니다. 이 자유도가 단순 카운터 루프와 FOR THEN UNTIL을 가르는 결정적 차이입니다.
1단계 — 기본 수열 생성 익히기
가장 단순한 형태부터 시작해 보겠습니다. 1부터 10까지의 정수를 담는 내부 테이블을 만드는 경우입니다.
CLASS zcl_for_then_demo DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_for_then_demo IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
TYPES tt_int TABLE OF i WITH EMPTY KEY.
"산술수열: 1, 2, 3, ..., 10
DATA(lt_seq_arith) = VALUE tt_int(
FOR seq_no = 1 THEN seq_no + 1 UNTIL seq_no > 10
( seq_no ) ).
"기하수열: 1, 2, 4, 8, 16, 32, 64
DATA(lt_seq_geom) = VALUE tt_int(
FOR power_val = 1 THEN power_val * 2 UNTIL power_val > 64
( power_val ) ).
"역방향 + 간격: 100, 90, 80, ..., 10
DATA(lt_seq_desc) = VALUE tt_int(
FOR step_val = 100 THEN step_val - 10 UNTIL step_val < 10
( step_val ) ).
out->write( lt_seq_arith ).
out->write( lt_seq_geom ).
out->write( lt_seq_desc ).
ENDMETHOD.
ENDCLASS.
주목할 점은 세 가지입니다. 첫째, 반복 변수(seq_no, power_val, step_val)는 자동으로 정수형으로 추론됩니다. 둘째, UNTIL 조건이 거짓인 동안만 본문이 실행되므로, 마지막에 추가되는 값은 조건을 막 통과하지 못한 값(예: 10, 64, 10)이 됩니다. 셋째, ( seq_no )는 행 생성자로 단일 컬럼 테이블의 한 행을 의미합니다. 다중 컬럼이면 ( col1 = ... col2 = ... ) 형태가 됩니다.
2단계 — 월별 분석 구간 생성과 예외 처리
실무에서 매우 자주 등장하는 시나리오를 봅시다. 사용자가 시작 회계월과 종료 회계월(YYYYMM 형식)을 입력하면, 해당 구간의 모든 월을 row로 갖는 분석 테이블을 만드는 경우입니다. 단순 정수 증가로는 안 됩니다. 202611 다음은 202612가 아니라 202701이 되어야 하기 때문입니다.
TYPES: BEGIN OF ty_period,
year_month TYPE n LENGTH 6,
year_part TYPE n LENGTH 4,
month_part TYPE n LENGTH 2,
is_year_end TYPE abap_bool,
END OF ty_period,
tt_period TYPE STANDARD TABLE OF ty_period WITH EMPTY KEY.
METHOD build_period_range.
IF iv_from > iv_to OR iv_from IS INITIAL.
RAISE EXCEPTION TYPE cx_parameter_invalid_range
EXPORTING parameter = 'IV_FROM'.
ENDIF.
TRY.
rt_periods = VALUE tt_period(
FOR month_cursor = iv_from
THEN COND #( WHEN month_cursor MOD 100 = 12
THEN ( ( month_cursor DIV 100 ) + 1 ) * 100 + 1
ELSE month_cursor + 1 )
UNTIL month_cursor > iv_to
( year_month = month_cursor
year_part = month_cursor DIV 100
month_part = month_cursor MOD 100
is_year_end = COND #( WHEN month_cursor MOD 100 = 12
THEN abap_true ELSE abap_false ) ) ).
CATCH cx_sy_arithmetic_error INTO DATA(lx_arith).
RAISE EXCEPTION TYPE zcx_period_calc_failed
EXPORTING previous = lx_arith.
ENDTRY.
ENDMETHOD.
여기서 THEN 식에 COND #( ... )를 끼워 넣은 점이 핵심입니다. FOR THEN UNTIL은 next_expr 자리에 임의의 expression을 받을 수 있으므로, 조건부 점화식도 가능합니다. 만약 점화식이 더 복잡해진다면(예: 영업일 기준 다음 월, 회계 캘린더 기반) 정적 메서드로 분리해 THEN cl_fiscal_utils=>next_period( month_cursor )처럼 호출해도 됩니다. 다만 메서드 호출은 매 반복마다 발생하므로 성능 영향을 측정하길 권합니다.
예외 처리는 두 갈래로 설계했습니다. 표현식 자체가 던질 수 있는 cx_sy_arithmetic_error를 잡아 도메인 예외(zcx_period_calc_failed)로 래핑하고, 입력 검증은 표현식 진입 전에 끝냅니다. 표현식 내부에서 raise를 시도하면 가독성이 급격히 떨어지므로, "검증 → 표현식 → 사후 후처리" 순서를 지키는 것을 권장합니다.
3단계 — 프로덕션 시나리오: 배치 슬라이싱과 REDUCE 결합
대용량 키 테이블을 N건씩 잘라 배치 처리하는 시나리오를 만들어 봅시다. 1,000만 건의 자재 키를 5,000건씩 슬라이스해 RFC로 보내는 패턴입니다. 슬라이스의 시작 인덱스만 수열로 생성하고, 본문에서는 VALUE로 부분 테이블을 구성합니다.
TYPES: BEGIN OF ty_batch_slice,
slice_no TYPE i,
start_index TYPE i,
end_index TYPE i,
keys TYPE matnr_tt,
END OF ty_batch_slice,
tt_batch_slice TYPE STANDARD TABLE OF ty_batch_slice WITH EMPTY KEY.
METHOD slice_for_batch.
CONSTANTS c_chunk_size TYPE i VALUE 5000.
DATA(lv_total) = lines( it_material_keys ).
IF lv_total = 0.
RETURN.
ENDIF.
rt_slices = VALUE tt_batch_slice(
FOR start_idx = 1
THEN start_idx + c_chunk_size
UNTIL start_idx > lv_total
LET end_idx = COND i( WHEN start_idx + c_chunk_size - 1 > lv_total
THEN lv_total
ELSE start_idx + c_chunk_size - 1 )
slice_seq = ( start_idx - 1 ) DIV c_chunk_size + 1
IN ( slice_no = slice_seq
start_index = start_idx
end_index = end_idx
keys = VALUE #(
FOR idx = start_idx THEN idx + 1 UNTIL idx > end_idx
( it_material_keys[ idx ] ) ) ) ).
DATA(lv_check_sum) = REDUCE i(
INIT s = 0
FOR <slice> IN rt_slices
NEXT s = s + lines( <slice>-keys ) ).
IF lv_check_sum <> lv_total.
RAISE EXCEPTION TYPE zcx_batch_slice_mismatch
EXPORTING expected = lv_total
actual = lv_check_sum.
ENDIF.
ENDMETHOD.
이 코드에서 짚어야 할 프로덕션 관점의 포인트는 다섯 가지입니다.
- LET ... IN으로 중간 계산 캡처:
end_idx와slice_seq를 LET으로 묶어 본문 가독성을 확보. LET 변수는 행 단위로 새로 계산되므로 부수효과가 없습니다 - 중첩 FOR: 외부 FOR는 슬라이스 시작 인덱스, 내부 FOR는 해당 슬라이스의 키들을 채웁니다. 중첩이 깊어지면 가독성이 떨어지므로 3단계 이상 중첩은 메서드 분리를 권장합니다
- 경계 조건:
end_idx를lv_total로 캡(cap)해 마지막 슬라이스가 부족한 건수만 갖도록 처리 - REDUCE로 사후 검증: 슬라이스 합계와 원본 건수가 일치하는지 즉시 검증. 인덱스 off-by-one 버그를 운영 단계 전에 차단합니다
- 상수화: 5000은
CONSTANTS로 추출. RFC 페이로드 한계나 메모리 프로파일에 따라 조정 가능하도록 분리
성능 측면에서 FOR THEN UNTIL 표현식은 내부적으로 컴파일러가 최적화된 루프 코드로 풀어내므로, 동일 로직의 DO/ENDDO + APPEND 대비 큰 차이는 없습니다. 다만 표현식 내부에서 it_material_keys[ idx ]처럼 표 표현식을 사용하면 STANDARD 테이블의 경우 선형 탐색이 발생할 수 있습니다. 위 예제는 인덱스 접근이라 O(1)이지만, 키 기반 접근이라면 SORTED/HASHED로 바꾸는 편이 안전합니다.
자주 마주치는 실수와 해결법
Q1. "Endless loop detected" 또는 응답 없음으로 멈춥니다.
가장 흔한 원인은 THEN 식이 반복 변수를 갱신하지 않거나, UNTIL 조건이 절대 참이 되지 않는 경우입니다. 예를 들어 FOR x = 1 THEN x * 2 UNTIL x < 0은 x가 음수가 될 수 없으므로 무한 루프입니다. 점화식과 종료 조건이 같은 방향(단조 증가/감소)으로 움직이는지 항상 검토하세요.
Q2. FOR WHILE과 FOR UNTIL은 어떻게 다르고 어느 쪽을 쓰나요?
동작은 정반대(WHILE=참인 동안 / UNTIL=참이 될 때까지)지만 표현력에는 차이가 없습니다. 팀 코딩 컨벤션상 "정상 종료 조건을 명시"하는 쪽을 권장합니다. "데이터가 더 있는 동안"이면 WHILE has_more가, "끝 인덱스를 넘어서면"이면 UNTIL idx > max가 자연스럽습니다.
Q3. DO/ENDDO + APPEND로도 똑같이 짤 수 있는데 굳이 FOR THEN UNTIL을 써야 하나요?
두 가지 기준으로 판단하세요. 첫째, "결과가 컬렉션(테이블/스칼라 누적값)인가?" Yes라면 VALUE/REDUCE + FOR가 의도를 더 잘 드러냅니다. 둘째, "반복 본문에 부수효과(DB 쓰기, 외부 호출)가 있는가?" Yes라면 DO/ENDDO가 적합합니다. 표현식은 본질적으로 "값을 계산하는 식"이므로 부수효과를 섞으면 가독성이 무너지고 디버깅도 어렵습니다.
Q4. 표현식 내부에서 예외가 발생하면 어디서 잡나요?
표현식 전체를 감싸는 TRY ... CATCH 블록에서 잡힙니다. 다만 어느 반복에서 터졌는지는 호출 스택만으로 식별이 어렵습니다. 따라서 사전 검증을 충실히 하거나, 의심 구간이면 표현식을 풀어 DO/ENDDO로 전환해 위치를 특정한 후 다시 표현식으로 되돌리는 워크플로를 권장합니다.
이어서 살펴볼 만한 주제
이 글에서 다룬 FOR THEN UNTIL을 익혔다면, 자연스럽게 다음 주제로 확장됩니다. 첫째, FOR ... IN(테이블 순회형)과의 조합 — 외부는 IN 루프, 내부는 THEN UNTIL로 행마다 수열을 만드는 패턴입니다. 둘째, REDUCE를 활용한 집계 함수 자체 구현 — 평균, 분산, 누적합을 한 표현식으로 처리하는 함수형 ABAP 스타일. 셋째, CDS View의 시스템 변수와 결합한 동적 회계 캘린더 생성. 마지막으로 RAP의 동작(action)/기능(function) 구현부에서 FOR THEN UNTIL을 활용한 임포트 파라미터 전개 패턴도 살펴볼 가치가 있습니다.
댓글 0
아직 댓글이 없습니다.