ABAP

아직도 DO LOOP? FOR THEN UNTIL 3단계 #shorts #SAP #ABAP

개요 및 도입

ABAP에서 연속된 숫자 시퀀스나 날짜 범위를 내부 테이블로 만들어야 하는 상황은 의외로 자주 발생합니다. 월별 매출 집계 슬롯, 회계연도 12개 기간, 발주일로부터 D+30까지의 일자 배열, 페이지네이션 인덱스 등이 대표적입니다. 전통적으로 DO ... ENDDO 루프로 처리하던 작업을 ABAP 7.40 SP08 이후 도입된 FOR ... THEN ... UNTIL / WHILE 패턴으로 한 줄에 표현할 수 있습니다. 이 글에서 다루는 항목은 다음과 같습니다.

  • FOR 표현식의 세 가지 형태(IN / UNTIL / WHILE) 비교
  • THEN 절을 활용한 증감 규칙 커스터마이징
  • VALUE/REDUCE 생성자와 결합한 범위 테이블 생성
  • 실무 시나리오: 회계 기간, 영업일 캘린더, 페이지 인덱스
  • 성능 비교(LOOP vs FOR), 디버깅 포인트, 단위 테스트 설계

사전에 알고 있어야 할 것

이 글은 ABAP 7.40 SP08 이상에서 도입된 표현식 기반 구문에 익숙한 개발자를 대상으로 합니다. VALUE #( ), NEW #( ), CONV, REDUCE 같은 생성자 표현식과 inline declaration(DATA(...)), table expression(itab[ ... ]) 개념을 이해하고 있다고 가정합니다. 또한 내부 테이블의 STANDARD/SORTED/HASHED 종류, BDC/CDS 기본 개념도 함께 알고 있으면 좋습니다.

환경 및 준비 사항

실습 환경은 다음 조합을 기준으로 합니다.

  • ABAP Platform 2022 또는 S/4HANA 2023 On-Premise (NetWeaver 7.55+)
  • ADT(ABAP Development Tools) for Eclipse 2024-03 이상
  • 최소 Kernel: 7.40 SP08 (FOR 표현식 최소 요구), 권장 7.55+
  • ABAP Cloud 환경(BTP ABAP Environment)에서도 동일 문법 사용 가능
  • 테스트는 ABAP Unit Test 프레임워크 사용

패키지는 $TMP가 아닌 transportable 패키지에 작성하는 것을 권장합니다. 일부 구식 시스템(7.40 SP05 이하)에서는 본문 예제 중 일부가 컴파일되지 않을 수 있으므로 버전 점검이 우선입니다.

핵심 개념: FOR 표현식의 세 갈래

ABAP의 FOR 표현식은 일반 LOOP AT의 인라인 버전이 아니라 "이터레이션 표현식(iteration expression)"입니다. 즉, 표현식이 평가되는 동안 결과 시퀀스를 만들어내고, 그것이 VALUEREDUCE의 입력이 됩니다. 형태는 세 가지입니다.

  • FOR ... IN itab: 기존 내부 테이블 원소를 순회 (LOOP AT과 유사)
  • FOR ... = start UNTIL cond: 시작값에서 조건이 참이 될 때까지 증감 (DO ... UNTIL과 유사)
  • FOR ... = start WHILE cond: 조건이 참인 동안 반복 (WHILE과 유사)

여기서 핵심이 THEN 절입니다. FOR i = 1 THEN i + 2 UNTIL i > 20처럼 쓰면 다음 반복의 i 값을 직접 정의할 수 있습니다. 기본 증분이 +1이 아닌 경우, 예를 들어 짝수만, 5의 배수만, 역방향, 날짜 +7일 단위 등 모든 시퀀스를 한 줄로 표현할 수 있습니다.

비유하자면 엑셀의 "채우기 핸들"입니다. 첫 셀에 1, 둘째 셀에 3을 넣고 드래그하면 5, 7, 9가 자동 생성되는 그 동작을 ABAP 구문으로 표현한 것이 THEN 절입니다. 컴파일러는 i의 초기값과 THEN의 변환식을 보고 매 반복마다 i를 재계산하며, UNTIL/WHILE 평가는 매 반복 시작 시점에 수행됩니다.

중요한 동작 원리 하나: FOR 표현식 내부의 변수(위 예에서 i)는 표현식 스코프 안에서만 살아있는 로컬 변수입니다. 외부에서 접근할 수 없으며, 표현식 종료 후에는 소멸합니다.

실전 예제 1: 기본 숫자 범위 만들기

먼저 가장 단순한 형태로 1부터 10까지의 정수 테이블, 그리고 2씩 증가하는 짝수 테이블을 만들어 봅니다.

REPORT zdemo_for_then_until_basic.

TYPES: BEGIN OF ty_seq,
         idx TYPE i,
       END OF ty_seq,
       tt_seq TYPE STANDARD TABLE OF ty_seq WITH EMPTY KEY.

" 1~10 단순 시퀀스
DATA(lt_one_to_ten) = VALUE tt_seq(
  FOR n = 1 UNTIL n > 10 ( idx = n ) ).

" 2부터 시작해 +2씩, 20 이하
DATA(lt_even) = VALUE tt_seq(
  FOR n = 2 THEN n + 2 UNTIL n > 20 ( idx = n ) ).

" 100부터 -10씩 감소, 0 이상까지
DATA(lt_desc) = VALUE tt_seq(
  FOR n = 100 THEN n - 10 UNTIL n < 0 ( idx = n ) ).

cl_demo_output=>new(
  )->write( lt_one_to_ten
  )->write( lt_even
  )->write( lt_desc
  )->display( ).

주의할 점은 UNTIL의 조건이 종료 조건이라는 것입니다. 조건이 true가 되는 순간 루프가 멈추며, 그 시점의 값은 결과에 포함되지 않습니다. 따라서 1~10을 만들고 싶다면 UNTIL n > 10이지 UNTIL n = 10이 아닙니다. WHILE로 같은 결과를 얻으려면 조건을 반전해 WHILE n <= 10으로 작성합니다.

실전 예제 2: 회계 기간 캘린더 + 에러 핸들링

실무에서 자주 등장하는 시나리오로, 특정 회계연도의 12개 기간(POPER)을 시작일·종료일·기간 라벨과 함께 생성합니다. 입력값 검증과 로깅을 곁들입니다.

CLASS zcl_fiscal_period_builder DEFINITION
  PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_period,
             poper      TYPE poper,
             date_from  TYPE datum,
             date_to    TYPE datum,
             label      TYPE string,
           END OF ty_period,
           tt_period TYPE STANDARD TABLE OF ty_period WITH EMPTY KEY.

    METHODS build_year
      IMPORTING iv_fiscyear     TYPE gjahr
      RETURNING VALUE(rt_period) TYPE tt_period
      RAISING   cx_parameter_invalid_range.
ENDCLASS.

CLASS zcl_fiscal_period_builder IMPLEMENTATION.
  METHOD build_year.

    IF iv_fiscyear < '2000' OR iv_fiscyear > '2099'.
      RAISE EXCEPTION TYPE cx_parameter_invalid_range
        EXPORTING
          parameter = 'IV_FISCYEAR'.
    ENDIF.

    DATA(lv_log) = NEW cl_bali_log_db_handler( ).

    rt_period = VALUE tt_period(
      FOR m = 1 THEN m + 1 UNTIL m > 12
        LET base_date = |{ iv_fiscyear }{ m WIDTH = 2 PAD = '0' }01|
        IN ( poper     = m
             date_from = CONV datum( base_date )
             date_to   = CONV datum( base_date ) + 30
             label     = |FY{ iv_fiscyear } P{ m WIDTH = 2 PAD = '0' }| ) ).

    MESSAGE |Generated { lines( rt_period ) } periods for { iv_fiscyear }|
            TYPE 'I'.
  ENDMETHOD.
ENDCLASS.

여기서 눈여겨볼 부분은 LET ... IN ( ... ) 절입니다. FOR 표현식 안에서 임시 변수를 선언해 매 반복마다 중간 계산값(여기서는 base_date)을 보관할 수 있습니다. 또한 CONV datum( ... )으로 문자열을 명시적으로 변환하지 않으면 implicit conversion 경고가 발생합니다. 실제 월말 일자는 FIMA_END_OF_MONTH_DETERMINE 같은 FM이나 CL_FHC_CALENDAR_RUNTIME을 통해 정확히 계산해야 하며, 위 +30은 데모용 단순화입니다.

실전 예제 3: 영업일 캘린더 + 성능/테스트/보안

마지막은 프로덕션 품질의 영업일(business day) 캘린더 생성기입니다. 주말과 공휴일을 제외한 일자 범위를 생성하고, 성능과 단위 테스트를 함께 고려합니다.

CLASS zcl_workday_range DEFINITION
  PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: tt_dates TYPE STANDARD TABLE OF datum WITH EMPTY KEY.

    METHODS generate
      IMPORTING iv_from    TYPE datum
                iv_to      TYPE datum
                iv_factory TYPE tfacid DEFAULT 'KR01'
      RETURNING VALUE(rt_dates) TYPE tt_dates
      RAISING   cx_abap_invalid_value.

  PRIVATE SECTION.
    METHODS is_workday
      IMPORTING iv_date     TYPE datum
                iv_factory  TYPE tfacid
      RETURNING VALUE(rv_ok) TYPE abap_bool.
ENDCLASS.

CLASS zcl_workday_range IMPLEMENTATION.

  METHOD generate.
    IF iv_from > iv_to.
      RAISE EXCEPTION TYPE cx_abap_invalid_value.
    ENDIF.

    DATA(lt_all) = VALUE tt_dates(
      FOR d = iv_from THEN d + 1 UNTIL d > iv_to ( d ) ).

    LOOP AT lt_all INTO DATA(lv_d)
         WHERE table_line IS NOT INITIAL.
      IF is_workday( iv_date = lv_d iv_factory = iv_factory ) = abap_true.
        APPEND lv_d TO rt_dates.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD is_workday.
    CALL FUNCTION 'DATE_CHECK_WORKINGDAY'
      EXPORTING
        date                       = iv_date
        factory_calendar_id        = iv_factory
        message_type               = 'I'
      EXCEPTIONS
        date_after_range           = 1
        date_before_range          = 2
        date_invalid               = 3
        date_no_workingday         = 4
        factory_calendar_not_found = 5
        message_type_invalid       = 6
        OTHERS                     = 7.
    rv_ok = COND #( WHEN sy-subrc = 0 THEN abap_true ELSE abap_false ).
  ENDMETHOD.
ENDCLASS.

CLASS ltc_workday DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS:
      test_one_week FOR TESTING,
      test_invalid_range FOR TESTING RAISING cx_static_check.
ENDCLASS.

CLASS ltc_workday IMPLEMENTATION.
  METHOD test_one_week.
    DATA(lo) = NEW zcl_workday_range( ).
    TRY.
        DATA(lt) = lo->generate(
          iv_from = '20260601' iv_to = '20260607' ).
        cl_abap_unit_assert=>assert_not_initial( lt ).
      CATCH cx_abap_invalid_value.
        cl_abap_unit_assert=>fail( ).
    ENDTRY.
  ENDMETHOD.

  METHOD test_invalid_range.
    DATA(lo) = NEW zcl_workday_range( ).
    TRY.
        lo->generate( iv_from = '20260610' iv_to = '20260601' ).
        cl_abap_unit_assert=>fail( ).
      CATCH cx_abap_invalid_value.
        " expected
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

프로덕션 관점에서 추가로 고려할 사항입니다. 첫째, 범위가 매우 크면(예: 10년치 일자) FOR가 메모리에 한 번에 테이블을 만들기 때문에 메모리 폭증이 발생할 수 있습니다. 이 경우 청크 단위로 잘라 처리하거나 스트리밍 가능한 CDS 뷰로 위임하는 것이 좋습니다. 둘째, 사용자 입력으로 from/to를 받는 경우 SQL Injection은 무관하지만 무한 범위 입력으로 인한 DoS 가능성이 있으므로 입력 검증을 반드시 추가합니다. 셋째, FOR 표현식은 ATC(ABAP Test Cockpit) 점검 시 가독성 검사에 걸릴 수 있으므로 복잡도가 일정 수준을 넘으면 헬퍼 메서드로 분리합니다.

흔한 실수와 트러블슈팅 FAQ

Q1. UNTIL과 WHILE을 헷갈려 빈 테이블이 나옵니다.
A. UNTIL은 "조건이 true가 되면 멈춤"이고 WHILE은 "조건이 true인 동안 계속". 초기값이 이미 종료 조건을 만족하면 결과는 0행입니다. FOR n = 10 UNTIL n > 5 ( n )은 빈 테이블을 반환합니다. 디버거에서 첫 반복 진입 여부를 확인하세요.

Q2. THEN 절에서 변수 이름을 다른 것으로 바꿔도 되나요?
A. 안 됩니다. THEN 다음에는 반드시 FOR에서 선언한 동일 식별자만 사용할 수 있습니다. FOR i = 1 THEN j + 1 ...은 컴파일 오류입니다. 또한 THEN 식은 i를 참조하지 않고 상수만 써도 되지만, 그 경우 무한 루프 위험이 있어 UNTIL 조건이 반드시 종료 가능해야 합니다.

Q3. LOOP보다 FOR가 항상 빠른가요?
A. 아닙니다. 같은 컴파일러가 비슷한 코드로 변환하므로 성능 차이는 대부분 무시할 수준입니다. 다만 FOR + VALUE 조합은 결과 테이블의 전체 크기를 컴파일러가 추정해 메모리를 한 번에 할당하므로, 매우 큰 테이블에서는 APPEND 반복보다 약간 유리할 수 있습니다. 반대로 중간에 조건부 break가 필요한 경우 LOOP가 더 적합합니다. SAT 트랜잭션으로 실측 후 결정하세요.

Q4. THEN 절에 함수 호출을 써도 되나요?
A. 순수 표현식이라면 가능하지만 외부 상태를 바꾸는 메서드는 피해야 합니다. 부수 효과(side effect)가 있으면 디버깅이 매우 어려워집니다. 또한 일부 시스템에서는 메서드 호출이 표현식 컨텍스트에서 제한될 수 있습니다.

이어서 살펴볼 주제

FOR 표현식에 익숙해졌다면 다음 단계로 REDUCE 표현식을 살펴보길 권장합니다. REDUCE는 FOR로 생성된 시퀀스를 단일 값(누적합, 최댓값, 문자열 연결 등)으로 축약하는 표현식이며 함수형 패러다임의 fold와 동일합니다. 또한 ABAP SQL의 WITH 절을 이용한 재귀 CTE로 DB 측에서 시퀀스를 만드는 방법, CDS Table Function으로 캘린더 뷰를 노출하는 방법도 함께 익히면 OData 서비스에서 바로 활용할 수 있습니다. RAP(RESTful ABAP Programming Model) 환경이라면 unmanaged action에서 FOR로 범위를 만들어 다건 처리하는 패턴도 유용합니다.

더 깊이 파볼 자료

댓글 0

아직 댓글이 없습니다.