ABAP

LOOP 없이 ABAP 집계 코드 90% 줄이기 #shorts #SAP #ABAP

REDUCE 연산자가 필요한 이유

ABAP 개발자라면 누구나 한 번쯤은 LOOP AT ... INTO ... WHERE ... 블록 안에서 누적 변수를 하나씩 더해본 경험이 있을 것입니다. 판매 주문 금액의 총합을 구하거나, 특정 자재 그룹의 재고 수량을 합산하거나, 평균 단가를 계산할 때 거의 반사적으로 떠오르는 패턴이죠. 그러나 이 방식은 코드 라인이 길어지고, 누적 변수 초기화를 깜빡하면 다음 호출에서 값이 더해지는 버그가 발생하기 쉽습니다. 또한 누적 변수의 스코프가 메서드 전체로 노출되어 사이드 이펙트가 생길 위험도 있습니다.

이 글에서는 ABAP 7.40 SP08부터 도입된 REDUCE 표현식을 활용해 LOOP 누적 패턴을 단 한 줄의 인라인 표현으로 압축하는 방법을 다룹니다. 다 읽고 나면 다음 항목들을 직접 코드에 적용할 수 있습니다.

  • REDUCE의 시드(seed) 값과 NEXT 절의 동작 원리 이해
  • 단일·복수 필드 집계를 인라인 표현식으로 작성
  • WHERE 조건과 결합한 필터 집계 패턴 구현
  • 중첩 REDUCE로 계층형 데이터 합산 처리
  • 리팩토링 시 LOOP와 REDUCE의 트레이드오프 판단

이 글을 읽기 전 알아두면 좋은 것

본문은 ABAP 7.40 SP08 이상의 신규 ABAP 구문(인라인 선언 DATA(...), FOR 표현식, VALUE 생성자)에 익숙하다는 전제 하에 진행합니다. 내부 테이블, 작업영역, FIELD-SYMBOL 개념과 함께 itab[ ... ] 테이블 표현식의 동작을 알고 있으면 더 빠르게 따라올 수 있습니다. CDS view에서의 집계가 아니라 ABAP 메모리 상의 내부 테이블 집계에 초점을 둡니다.

실습 환경과 준비물

예제는 SAP NetWeaver AS ABAP 7.55 (S/4HANA 2022) 기준으로 검증했으며, 7.40 SP08 이상의 모든 릴리스에서 동작합니다. ABAP Cloud(Steampunk, ABAP Environment) 및 RAP 기반 구현에서도 동일한 문법이 적용되므로 온프레미스와 클라우드 양쪽 모두에서 사용 가능합니다. 개발 도구는 ADT(ABAP Development Tools) 3.36 이상을 권장하며, ATC 체크에서 "Modern ABAP Language" 카테고리를 활성화해두면 LOOP-누적 패턴을 자동으로 감지해 REDUCE 리팩토링 힌트를 받을 수 있습니다.

실습 데이터는 다음과 같은 구조체 타입을 가정합니다.

TYPES: BEGIN OF ty_sales_item,
         order_id    TYPE vbeln_va,
         item_no     TYPE posnr_va,
         material    TYPE matnr,
         plant       TYPE werks_d,
         net_amount  TYPE netwr_ap,
         currency    TYPE waers,
         quantity    TYPE menge_d,
         uom         TYPE meins,
       END OF ty_sales_item,
       tt_sales_item TYPE STANDARD TABLE OF ty_sales_item WITH EMPTY KEY.

REDUCE 문법의 동작 원리

REDUCE는 함수형 프로그래밍의 fold(혹은 reduce) 개념을 ABAP에 도입한 표현식입니다. 핵심 아이디어는 "초기값(seed)을 출발점으로 잡고, 반복할 때마다 다음 상태값(NEXT)을 계산해 누적하다가, 반복이 끝난 시점의 최종 값을 표현식 전체의 반환값으로 돌려준다"는 것입니다. 누적 변수를 메서드 스코프에 노출하지 않고, 표현식 내부에서만 살아 있다가 사라지므로 사이드 이펙트가 원천 차단됩니다.

기본 형태는 다음과 같습니다.

DATA(result) = REDUCE result_type(
                 INIT acc = initial_value
                 FOR  wa IN itab [WHERE ( cond )]
                 NEXT acc = expression( acc, wa ) ).

각 절의 역할을 풀어 보면 다음과 같습니다.

  • result_type: 최종 결과의 타입. 스칼라(i, p)일 수도, 구조체나 내부 테이블일 수도 있습니다.
  • INIT: 누적기(accumulator) 변수의 초기값. 변수 이름은 자유롭게 정의 가능.
  • FOR ... IN: 순회 대상 내부 테이블과 작업영역.
  • WHERE: 선택적 필터. SQL의 WHERE처럼 조건을 만족하는 행만 누적에 참여.
  • NEXT: 다음 단계의 누적기 값을 계산하는 표현식. 이 표현식의 마지막 결과가 표현식 전체의 반환값.

1단계 - 단순 합계 집계 실전 예제

가장 흔한 시나리오는 판매 주문 라인의 순매출 합계를 구하는 것입니다. 먼저 익숙한 LOOP 방식으로 작성해 봅니다.

DATA lv_total_amount TYPE netwr_ap.
CLEAR lv_total_amount.
LOOP AT lt_sales_items INTO DATA(ls_item).
  lv_total_amount = lv_total_amount + ls_item-net_amount.
ENDLOOP.

같은 결과를 REDUCE로 표현하면 단 한 줄입니다.

DATA(lv_total_amount) = REDUCE netwr_ap(
                          INIT sum = 0
                          FOR  ls_item IN lt_sales_items
                          NEXT sum  = sum + ls_item-net_amount ).

여기서 sum은 표현식 내부에서만 살아있는 임시 변수입니다. 누적 변수가 외부에 노출되지 않으니 CLEAR를 잊어서 발생하는 누적 오염 버그가 사라집니다.

2단계 - 복수 필드 동시 집계

실무에서는 합계 하나만 필요한 경우는 드뭅니다. 대시보드나 리포트를 위해 합계, 건수, 최대값, 평균을 한 번에 산출해야 할 때가 많죠. 이때는 누적기를 구조체로 정의합니다.

TYPES: BEGIN OF ty_aggregate,
         total_amount TYPE netwr_ap,
         total_qty    TYPE menge_d,
         item_count   TYPE i,
         max_amount   TYPE netwr_ap,
       END OF ty_aggregate.

DATA(ls_stats) = REDUCE ty_aggregate(
                   INIT agg = VALUE ty_aggregate( )
                   FOR  ls_item IN lt_sales_items
                   NEXT agg-total_amount = agg-total_amount + ls_item-net_amount
                        agg-total_qty    = agg-total_qty    + ls_item-quantity
                        agg-item_count   = agg-item_count   + 1
                        agg-max_amount   = COND netwr_ap(
                                             WHEN ls_item-net_amount > agg-max_amount
                                             THEN ls_item-net_amount
                                             ELSE agg-max_amount ) ).

DATA(lv_avg_amount) = COND netwr_ap(
                        WHEN ls_stats-item_count > 0
                        THEN ls_stats-total_amount / ls_stats-item_count
                        ELSE 0 ).

NEXT 절에는 여러 필드 할당을 공백으로 구분해 나열할 수 있습니다. 평균은 0 division을 막기 위해 별도로 COND로 처리했습니다. 이 한 표현식으로 4가지 통계가 한 번의 순회로 산출되며, LOOP 버전 대비 코드량이 절반 이하로 줄어듭니다.

3단계 - 조건부 필터 집계와 그룹별 집계

특정 플랜트나 통화에 한정해 집계해야 하는 경우 WHERE 절을 그대로 활용합니다. 다음 예제는 플랜트 '1000'이면서 통화가 'EUR'인 라인만 합산합니다.

DATA(lv_eur_total) = REDUCE netwr_ap(
                       INIT sum = 0
                       FOR  ls_item IN lt_sales_items
                            WHERE ( plant    = '1000'
                                AND currency = 'EUR' )
                       NEXT sum = sum + ls_item-net_amount ).

플랜트별 합계 테이블을 한 번에 만들고 싶다면 결과 타입을 내부 테이블로 두고 라인이 있는지 확인 후 갱신하는 패턴도 가능합니다.

TYPES: BEGIN OF ty_plant_sum,
         plant        TYPE werks_d,
         total_amount TYPE netwr_ap,
       END OF ty_plant_sum,
       tt_plant_sum TYPE SORTED TABLE OF ty_plant_sum WITH UNIQUE KEY plant.

DATA(lt_by_plant) =
  REDUCE tt_plant_sum(
    INIT result = VALUE tt_plant_sum( )
    FOR  ls_item IN lt_sales_items
    NEXT result = VALUE #(
      BASE result
      ( plant        = ls_item-plant
        total_amount = COND netwr_ap(
                         LET existing = line_exists( result[ plant = ls_item-plant ] ) IN
                         WHEN existing = abap_true
                         THEN result[ plant = ls_item-plant ]-total_amount + ls_item-net_amount
                         ELSE ls_item-net_amount ) ) ) ).

중첩 REDUCE로 서브테이블 집계

판매 주문 헤더 테이블 각 라인에 아이템 테이블이 깊이 포함된 계층 구조를 다룬다고 합시다. 헤더 전체에 걸쳐 모든 아이템 금액의 총합을 구하려면 REDUCE를 중첩할 수 있습니다.

TYPES: BEGIN OF ty_order_header,
         order_id TYPE vbeln_va,
         items    TYPE tt_sales_item,
       END OF ty_order_header,
       tt_order_header TYPE STANDARD TABLE OF ty_order_header WITH EMPTY KEY.

DATA(lv_grand_total) =
  REDUCE netwr_ap(
    INIT outer = 0
    FOR  ls_header IN lt_orders
    NEXT outer = outer + REDUCE netwr_ap(
                           INIT inner = 0
                           FOR  ls_item IN ls_header-items
                           NEXT inner = inner + ls_item-net_amount ) ).

외부 REDUCE의 NEXT 절 안에서 내부 REDUCE를 호출해 각 헤더의 소계를 만든 뒤 그 값을 outer에 더하는 구조입니다. 중첩 LOOP를 두 번 쓰고 누적 변수를 두 개 관리하던 패턴이 한 표현식으로 압축됩니다.

흔한 실수와 트러블슈팅

Q1. CX_SY_ITAB_LINE_NOT_FOUND가 NEXT 절에서 발생합니다.
누적기 테이블에서 result[ key = ... ]로 라인을 직접 참조할 때, 존재하지 않으면 예외가 발생합니다. line_exists( ) 또는 line_index( )로 먼저 확인하거나, VALUE #( ... OPTIONAL ) 패턴을 함께 사용해야 합니다.

Q2. NEXT 절에서 산술 오버플로(CX_SY_ARITHMETIC_OVERFLOW)가 납니다.
누적 결과 타입이 너무 작은 패킹(P)일 때 자주 발생합니다. INIT sum = CONV netwr_ap( 0 )처럼 명시적으로 타입을 부여하거나 결과 타입을 충분히 큰 패킹으로 잡아야 합니다.

Q3. 빈 테이블에서 REDUCE 결과가 예상과 다릅니다.
순회 대상이 비어 있으면 NEXT는 한 번도 실행되지 않고 INIT 값이 그대로 반환됩니다. 평균·최대값을 구할 때는 INIT 값을 분기 처리하거나 호출 이후 COND로 후처리하는 방어 코드를 두는 것이 일반적입니다.

Q4. REDUCE가 LOOP보다 항상 빠른가요?
컴파일러가 REDUCE를 내부적으로 LOOP 유사 구조로 변환하므로 큰 차이는 없는 것이 일반적입니다. 단, 누적기를 매번 VALUE #( BASE ... )로 재생성하는 패턴은 메모리 카피가 누적되어 오히려 느려질 수 있습니다. 대용량 그룹 집계는 LOOP + FIELD-SYMBOL 또는 SQL 위임을 권장합니다.

관련해서 더 살펴볼 주제

REDUCE에 익숙해졌다면 동일 계열의 생성자 표현식을 함께 익혀두면 시너지가 큽니다. FOR ... IN ... GROUP BY 구문은 REDUCE 단독으로 처리하기 까다로운 그룹 집계를 깔끔하게 풀어줍니다. FILTER 표현식은 별도 LOOP 없이 조건에 맞는 라인만 추출할 때 유용하고, CORRESPONDING 표현식은 매핑·변환 시 LOOP를 대체합니다. 더 나아가 ABAP SQL의 집계 함수(SUM, AVG, MAX)나 CDS view의 @Aggregation.default 어노테이션으로 집계를 데이터베이스 측으로 푸시다운하는 패턴까지 익히면, 메모리·DB 어느 계층에서든 가장 효율적인 도구를 선택할 수 있게 됩니다.

댓글 0

아직 댓글이 없습니다.