ABAP

FOR GROUPS BY vs COLLECT — 집계 차이 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이번 예제에서 다룰 내용

ABAP 7.40 SP08부터 도입된 FOR ... GROUPS BY 표현은 내부 테이블을 키 기반으로 그룹화하고 각 그룹별로 집계·요약 데이터를 한 번의 표현으로 생성할 수 있게 해주는 강력한 구문입니다. 기존에는 COLLECT, LOOP AT ... GROUP BY, 보조 테이블 변수 등을 조합해야 했지만, FOR GROUPS BY를 사용하면 선언형 스타일로 그룹별 집계 결과 테이블을 즉시 만들 수 있습니다. 이 글에서는 영업 주문(SalesOrder) 데이터를 고객별·지역별로 그룹화하고 매출 합계, 평균, 최대값을 계산하는 시나리오를 통해 실무에서 어떻게 활용하는지 단계별로 살펴봅니다.

  • FOR GROUPS BY와 기존 COLLECT / LOOP GROUP BY의 차이점 이해
  • 단일 키, 복합 키, 동적 그룹 키 정의 방법
  • REDUCE와 결합하여 그룹별 집계 계산
  • 대량 데이터에서의 성능·메모리 고려사항
  • 단위 테스트로 그룹 집계 결과 검증하기

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

ABAP 7.40 이후의 표현식 기반 문법(VALUE, REDUCE, FOR, COND)에 익숙해야 합니다. 또한 내부 테이블 종류(STANDARD / SORTED / HASHED)와 LOOP AT ... GROUP BY의 동작 원리, 인라인 선언(DATA(...)), 그리고 구조체·테이블 타입 정의 방법을 알고 있어야 본 예제를 무리 없이 따라갈 수 있습니다.

실습 환경과 준비물

본 예제는 다음 환경을 기준으로 검증했습니다. 권장 환경이며, 7.40 SP08 이상이면 기본 문법은 동일하게 동작합니다.

  • SAP NetWeaver AS ABAP 7.55 이상 또는 ABAP Platform 2022 (S/4HANA 2022 / BTP ABAP Environment)
  • ABAP Development Tools (ADT) for Eclipse 2024-03 이상
  • 최소 버전: ABAP 7.40 SP08 (단, 7.50 이후 일부 확장 구문 사용 가능)
  • 테스트 프레임워크: ABAP Unit (SE80 또는 ADT 내장)
  • 샘플 데이터: 사내 영업 주문 테이블 또는 직접 생성한 인라인 데이터

S/4HANA Cloud 환경에서도 동일하게 사용 가능하지만, RAP 모델 내부 동작 핸들러(behavior implementation)에서 활용할 때는 트랜잭션 버퍼와의 일관성에 유의해야 합니다.

핵심 개념과 동작 원리

FOR GROUPS BY는 일반적으로 VALUE 또는 REDUCE 내부에서 사용되며, 입력 테이블을 한 번 순회하면서 지정한 그룹 키에 따라 "그룹 대표 라인"을 생성합니다. 비유하자면 우체국에서 우편물을 지역별 바구니에 분류하는 것과 같습니다. 분류기는 한 번만 돌아가지만 결과적으로 각 지역에 해당하는 묶음이 만들어지고, 각 묶음 내부에서 다시 집계(무게 합계, 개수)를 계산할 수 있습니다.

구조적으로는 두 단계로 구분됩니다.

  1. 그룹 식별 단계: FOR GROUPS <group> OF <wa> IN itab GROUP BY ( key1 = wa-f1 key2 = wa-f2 ) 형식으로 그룹 키를 정의합니다. 결과로 각 고유 키 조합당 하나의 그룹 핸들이 생성됩니다.
  2. 그룹 처리 단계: 각 그룹 핸들에 대해 다시 FOR member IN GROUP <group>으로 내부 멤버를 순회하면서 합계/평균/카운트를 계산하거나, 그룹 정보 자체를 결과 라인으로 매핑합니다.

전통적인 COLLECT는 숫자 컴포넌트만 누적 가능하고 키 컬럼을 자동으로 식별하는 제약이 있는 반면, FOR GROUPS BY는 임의 표현식을 키로 사용할 수 있고 비숫자 집계(최대/최소 문자열, 멤버 개수, 첫·마지막 라인 등)도 처리할 수 있습니다. 또한 LOOP AT GROUP BY가 명령형(절차) 스타일이라면 FOR GROUPS BY는 선언형(결과 중심)이라는 점이 가장 큰 차이입니다.

내부적으로 ABAP 런타임은 그룹 키에 대한 해시 인덱스를 사용해 그룹을 식별하므로, 동일한 키를 가진 라인이 인접해 있지 않아도 동작합니다. 다만 GROUP BY ... WITHOUT MEMBERS 옵션을 명시하면 그룹 내부 멤버 접근을 포기하는 대신 메모리 사용을 줄일 수 있습니다.

1단계 — 기본 형태: 고객별 주문 건수 집계

첫 번째 예제는 가장 단순한 형태로, 영업 주문 테이블을 고객 ID(customer_id) 기준으로 그룹화하고 각 고객의 주문 건수를 산출합니다.

TYPES: BEGIN OF ty_sales_order,
         order_id    TYPE c LENGTH 10,
         customer_id TYPE c LENGTH 8,
         region      TYPE c LENGTH 4,
         net_amount  TYPE p LENGTH 11 DECIMALS 2,
         currency    TYPE c LENGTH 3,
       END OF ty_sales_order.

TYPES: tt_sales_order TYPE STANDARD TABLE OF ty_sales_order WITH EMPTY KEY.

TYPES: BEGIN OF ty_customer_summary,
         customer_id  TYPE c LENGTH 8,
         order_count  TYPE i,
       END OF ty_customer_summary.

TYPES: tt_customer_summary TYPE STANDARD TABLE OF ty_customer_summary
                           WITH EMPTY KEY.

DATA(lt_orders) = VALUE tt_sales_order(
  ( order_id = 'SO0000001' customer_id = 'C0001' region = 'APJ'
    net_amount = '1200.00' currency = 'EUR' )
  ( order_id = 'SO0000002' customer_id = 'C0002' region = 'EMEA'
    net_amount = '850.50'  currency = 'EUR' )
  ( order_id = 'SO0000003' customer_id = 'C0001' region = 'APJ'
    net_amount = '430.00'  currency = 'EUR' )
  ( order_id = 'SO0000004' customer_id = 'C0003' region = 'NA'
    net_amount = '2750.75' currency = 'USD' )
  ( order_id = 'SO0000005' customer_id = 'C0002' region = 'EMEA'
    net_amount = '99.90'   currency = 'EUR' ) ).

DATA(lt_summary) = VALUE tt_customer_summary(
  FOR GROUPS <grp> OF <order> IN lt_orders
      GROUP BY ( customer_id = <order>-customer_id )
      ( customer_id = <grp>-customer_id
        order_count = REDUCE i( INIT cnt = 0
                                FOR <m> IN GROUP <grp>
                                NEXT cnt = cnt + 1 ) ) ).

결과 테이블 lt_summary는 고객별로 한 줄씩, 주문 건수가 정확히 집계된 상태로 생성됩니다. COLLECT를 쓰면 추가 변수와 명령형 루프가 필요한데, 표현식 한 번으로 처리된 점에 주목하세요.

2단계 — 복합 키 + 다중 집계 + 예외 처리

실무에서는 단일 키 그룹이 거의 없습니다. 두 번째 예제는 (고객, 지역) 복합 키로 그룹화하고 합계·평균·최대값을 동시에 계산하며, 통화가 섞여 있을 때 예외를 발생시키는 견고한 패턴을 보여줍니다.

TYPES: BEGIN OF ty_sales_kpi,
         customer_id TYPE c LENGTH 8,
         region      TYPE c LENGTH 4,
         currency    TYPE c LENGTH 3,
         total_amt   TYPE p LENGTH 13 DECIMALS 2,
         avg_amt     TYPE p LENGTH 13 DECIMALS 2,
         max_amt     TYPE p LENGTH 13 DECIMALS 2,
         line_cnt    TYPE i,
       END OF ty_sales_kpi.

TYPES: tt_sales_kpi TYPE SORTED TABLE OF ty_sales_kpi
                    WITH UNIQUE KEY customer_id region.

CLASS lcx_currency_mismatch DEFINITION INHERITING FROM cx_static_check.
  PUBLIC SECTION.
    DATA mv_customer TYPE c LENGTH 8.
    DATA mv_region   TYPE c LENGTH 4.
ENDCLASS.

METHOD aggregate_sales.
  " 입력: it_orders TYPE tt_sales_order
  " 출력: rt_kpi    TYPE tt_sales_kpi
  " 예외: lcx_currency_mismatch

  rt_kpi = VALUE tt_sales_kpi(
    FOR GROUPS <grp> OF <order> IN it_orders
        GROUP BY ( customer_id = <order>-customer_id
                   region      = <order>-region
                   size        = GROUP SIZE
                   index       = GROUP INDEX )
        LET lt_members = VALUE tt_sales_order(
              FOR <m> IN GROUP <grp> ( <m> ) )
            lv_curr    = lt_members[ 1 ]-currency
        IN ( customer_id = <grp>-customer_id
             region      = <grp>-region
             currency    = lv_curr
             line_cnt    = <grp>-size
             total_amt   = REDUCE p( INIT s TYPE p LENGTH 13 DECIMALS 2
                                     FOR <m> IN GROUP <grp>
                                     NEXT s = s + <m>-net_amount )
             max_amt     = REDUCE p( INIT mx TYPE p LENGTH 13 DECIMALS 2
                                     FOR <m> IN GROUP <grp>
                                     NEXT mx = COND #( WHEN <m>-net_amount > mx
                                                       THEN <m>-net_amount
                                                       ELSE mx ) )
             avg_amt     = REDUCE p( INIT s TYPE p LENGTH 13 DECIMALS 2
                                     FOR <m> IN GROUP <grp>
                                     NEXT s = s + <m>-net_amount )
                           / <grp>-size ) ).

  " 통화 일관성 사후 검증
  LOOP AT rt_kpi ASSIGNING FIELD-SYMBOL(<kpi>).
    DATA(lt_check) = FILTER #( it_orders
                       WHERE customer_id = <kpi>-customer_id
                         AND region      = <kpi>-region ).
    IF line_exists( lt_check[ currency <> <kpi>-currency ] ).
      RAISE EXCEPTION TYPE lcx_currency_mismatch
        EXPORTING mv_customer = <kpi>-customer_id
                  mv_region   = <kpi>-region.
    ENDIF.

    cl_demo_output=>write( |Group { <kpi>-customer_id }/{ <kpi>-region }: | &&
                          |count={ <kpi>-line_cnt } total={ <kpi>-total_amt }| ).
  ENDLOOP.
ENDMETHOD.

여기서 주목할 점은 GROUP SIZEGROUP INDEX 가상 컬럼입니다. 그룹의 멤버 수와 순번을 별도 카운터 없이 얻을 수 있어 평균 계산 시 분모로 바로 사용할 수 있습니다. 또한 LET ... IN 절을 활용해 임시 변수(첫 멤버 통화)를 깔끔하게 한정 범위로 선언했습니다.

3단계 — 프로덕션 패턴: 성능 최적화와 단위 테스트

대용량 데이터에서 FOR GROUPS BY를 사용할 때는 입력 테이블 종류 선택, 멤버 미사용 옵션, 그리고 테스트 가능성이 중요합니다. 세 번째 예제는 100만 건 이상 처리를 가정한 패턴과 ABAP Unit 테스트입니다.

CLASS zcl_sales_aggregator DEFINITION
  PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_region_kpi,
             region    TYPE c LENGTH 4,
             total_amt TYPE p LENGTH 15 DECIMALS 2,
             order_cnt TYPE i,
           END OF ty_region_kpi.
    TYPES: tt_region_kpi TYPE SORTED TABLE OF ty_region_kpi
                         WITH UNIQUE KEY region.

    METHODS aggregate_by_region
      IMPORTING it_orders     TYPE tt_sales_order
      RETURNING VALUE(rt_kpi) TYPE tt_region_kpi.
ENDCLASS.

CLASS zcl_sales_aggregator IMPLEMENTATION.
  METHOD aggregate_by_region.
    rt_kpi = VALUE #(
      FOR GROUPS <grp> OF <o> IN it_orders
          GROUP BY ( region = <o>-region
                     size   = GROUP SIZE )
          ( region    = <grp>-region
            order_cnt = <grp>-size
            total_amt = REDUCE p( INIT s TYPE p LENGTH 15 DECIMALS 2
                                  FOR <m> IN GROUP <grp>
                                  NEXT s = s + <m>-net_amount ) ) ).
  ENDMETHOD.
ENDCLASS.

CLASS ltc_aggregator DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    DATA mo_cut TYPE REF TO zcl_sales_aggregator.
    METHODS:
      setup,
      empty_input_returns_empty FOR TESTING,
      sums_by_region            FOR TESTING.
ENDCLASS.

CLASS ltc_aggregator IMPLEMENTATION.
  METHOD setup.
    mo_cut = NEW #( ).
  ENDMETHOD.

  METHOD empty_input_returns_empty.
    DATA(lt_in) = VALUE tt_sales_order( ).
    cl_abap_unit_assert=>assert_initial( mo_cut->aggregate_by_region( lt_in ) ).
  ENDMETHOD.

  METHOD sums_by_region.
    DATA(lt_in) = VALUE tt_sales_order(
      ( order_id = 'A' customer_id = 'C1' region = 'APJ'
        net_amount = '100.00' currency = 'EUR' )
      ( order_id = 'B' customer_id = 'C2' region = 'APJ'
        net_amount = '250.00' currency = 'EUR' )
      ( order_id = 'C' customer_id = 'C3' region = 'NA'
        net_amount = '700.00' currency = 'USD' ) ).

    DATA(lt_out) = mo_cut->aggregate_by_region( lt_in ).

    cl_abap_unit_assert=>assert_equals(
      act = lt_out[ region = 'APJ' ]-total_amt
      exp = CONV p( '350.00' ) ).
    cl_abap_unit_assert=>assert_equals(
      act = lt_out[ region = 'APJ' ]-order_cnt
      exp = 2 ).
  ENDMETHOD.
ENDCLASS.

프로덕션 적용 시 다음 사항을 권장합니다.

  • 입력 테이블 키: 그룹 키가 SORTED 또는 HASHED 키와 일치하면 일반적으로 더 빠릅니다.
  • 결과 테이블 키: SORTED + UNIQUE KEY로 선언하면 후속 조회 비용이 줄어듭니다.
  • WITHOUT MEMBERS: 그룹별 카운트나 키만 필요하면 멤버 재순회를 생략해 메모리를 절약합니다.
  • 예외 클래스: 정적(STATIC_CHECK) 예외로 호출자가 처리하도록 강제합니다.
  • 로깅: BAL_LOG_* 또는 애플리케이션 로그 클래스로 그룹 처리 결과를 기록합니다.

흔히 마주치는 함정과 해결법

Q1. FOR GROUPS BY 결과가 비어 있거나 컴파일이 안 됩니다.
가장 흔한 원인은 그룹 처리 식에서 그룹 핸들(<grp>)이 아닌 원본 워크 에어리어(<order>)를 참조하는 경우입니다. 그룹 키가 만들어진 이후에는 반드시 <grp>-필드명으로 접근해야 합니다. 멤버 값을 쓰고 싶다면 내부의 FOR <m> IN GROUP <grp>를 통해 접근하세요.

Q2. COLLECT와 결과가 다릅니다.
COLLECT는 모든 비숫자 필드가 키로 간주되지만 FOR GROUPS BY는 명시한 키만 그룹 기준이 됩니다. 의도하지 않은 컬럼이 결과에 포함되면 그룹이 더 잘게 쪼개지거나, 반대로 키에 빠진 컬럼은 그룹 내 첫 값/마지막 값 중 무엇이 들어갈지 불명확해질 수 있으니 LET 절에서 명시적으로 추출해야 합니다.

Q3. 그룹 개수는 많은데 멤버는 1~2개씩만 있는 경우 성능이 떨어집니다.
그룹 식별 + 멤버 재순회의 오버헤드가 데이터 처리 자체보다 커질 수 있습니다. 이런 경우는 SELECT 단계에서 GROUP BY로 DB에 위임하거나, AMDP / CDS View의 집계 함수를 사용하는 편이 일반적으로 더 효율적입니다. ABAP 메모리 안에서 굳이 처리해야 한다면 입력 테이블을 HASHED로 만들거나 정렬해 두는 전처리를 권장합니다.

Q4. GROUP SIZE가 컴파일 오류를 냅니다.
GROUP SIZE, GROUP INDEX는 GROUP BY 절 안에서 명명된 컴포넌트로 선언한 뒤(size = GROUP SIZE) 그룹 처리 식에서 <grp>-size로 참조해야 합니다. 그룹 처리 식에서 바로 GROUP SIZE를 쓰면 인식되지 않습니다.

이후 살펴볼만한 관련 주제

그룹 집계를 마스터했다면 다음 주제로 확장해보길 권장합니다. 첫째, CDS View의 GROUP BY와 집계 함수 / 윈도우 함수를 사용한 DB 푸시다운 패턴. 둘째, AMDP(ABAP Managed Database Procedure)로 HANA SQLScript를 호출해 대용량 집계를 위임하는 방법. 셋째, RAP(Restful ABAP Programming Model)의 Determination/Validation에서 FOR GROUPS BY를 활용해 헤더-아이템 합계를 일관성 있게 유지하는 패턴. 넷째, REDUCEFILTER를 조합한 함수형 ABAP 패턴 전반. 다섯째, ABAP CDS Table Functions로 ABAP 로직과 SQL 집계를 혼합하는 고급 시나리오.

더 읽어볼 자료

댓글 0

아직 댓글이 없습니다.