ABAP

FILTER vs WHERE vs 루프 — 뭘 써야 빠를까? #shorts #SAP #ABAP

세 가지 방법이 존재하는 이유 — ABAP 컬렉션 필터의 역사

ABAP에서 내부 테이블을 다루는 개발자라면 누구나 한 번쯤 "이 컬렉션에서 특정 조건에 맞는 행만 뽑아내려면 어떻게 하는 게 가장 좋을까?"라는 질문을 마주칩니다. 흥미로운 점은 ABAP 언어가 시간이 흐르면서 이 문제를 해결하는 방법을 3세대에 걸쳐 확장해 왔다는 것입니다. 초창기의 절차적 스타일부터 최신 함수형 표현식까지, 각 방식은 그 시대의 언어 철학과 성능 요구사항을 반영합니다.

가장 오래된 방식은 LOOP AT ... WHERE 구문으로, R/3 시절부터 존재해 온 ABAP의 근본입니다. 다음 세대는 ABAP 7.40 SP08에서 도입된 FOR ... IN ... WHERE 인라인 구성 표현으로, 함수형 프로그래밍 패러다임을 ABAP에 이식했습니다. 마지막으로 ABAP 7.40 SP08에서 함께 등장한 FILTER 연산자는 sorted/hashed 테이블의 인덱스를 활용해 극한의 성능을 노리는 특수 도구입니다.

이 글에서는 이 세 방식의 내부 동작 원리, 성능 특성, 그리고 실무에서 SalesOrder 같은 대용량 데이터를 다룰 때의 선택 기준을 다룹니다.

미리 알아두면 좋은 배경

이 글은 ABAP 내부 테이블(standard/sorted/hashed table), 테이블 키(primary/secondary key)의 개념을 이미 알고 있는 개발자를 대상으로 합니다. 추가로 ABAP 7.40 이후의 인라인 선언(DATA(...)), VALUE/FOR/REDUCE 같은 컨스트럭터 연산자에 익숙하면 이해가 훨씬 빠릅니다. 새로운 문법이 낯설다면 SAP Help의 "Iteration Expressions" 문서를 먼저 훑고 오는 것을 권장합니다.

실습 환경과 준비물

실습에서 사용한 환경은 다음과 같습니다. FILTER 연산자와 FOR ... IN ... WHERE는 ABAP 7.40 SP08 이상에서만 동작하므로, ECC 6.0 EhP7 이전 시스템에서는 첫 번째 방식만 활용 가능합니다.

  • ABAP Platform 2023 (S/4HANA 2023 온프레미스) 또는 SAP BTP ABAP Environment
  • ABAP 언어 버전: Standard ABAP 7.58 (하위 호환: 7.40 SP08 이상)
  • 개발 도구: ADT (Eclipse) 2024-03 또는 SE80
  • 테스트 데이터: 사내 ZSALES_ORDER 테이블 또는 SFLIGHT/SBOOK
  • 성능 측정: GET RUN TIME FIELD, SAT(ABAP Runtime Analysis)

본문 코드는 On-Stack 인메모리 필터링에 초점을 맞춥니다. DB 레벨 필터링(SELECT ... WHERE)은 성격이 다르므로 별도 논의로 남깁니다.

핵심 개념 — 세 방식의 동작 원리

세 방식을 이해하는 열쇠는 "컴파일 시점에 어떤 최적화를 할 수 있는가""어떤 인덱스를 활용할 수 있는가"입니다. 일종의 도구 상자에 비유하면, LOOP는 다목적 스위스 나이프, FOR IN WHERE는 매끄러운 함수형 렌치, FILTER는 특정 볼트에만 맞는 초정밀 소켓 렌치입니다.

1) LOOP AT itab WHERE ...

가장 전통적인 방식으로, 커서를 순차적으로 이동하면서 WHERE 절과 매칭되는 행을 방문합니다. Standard table에서는 O(n) 풀 스캔이지만, sorted table에서 primary key의 접두 조건을 걸면 이진 탐색으로 O(log n)까지 단축됩니다. Hashed table의 primary key 완전 일치라면 O(1)이지만, 부분 조건이 들어가면 hash를 활용할 수 없어 결국 풀 스캔으로 떨어집니다.

2) FOR wa IN itab WHERE (...)

ABAP 7.40 SP08에서 도입된 iteration 표현으로, VALUE, REDUCE, NEW 같은 컨스트럭터 안에서 새로운 컬렉션을 만들어 냅니다. 내부적으로는 컴파일러가 LOOP로 변환하기 때문에 순수 성능 관점에서는 LOOP AT ... WHERE와 거의 동등합니다. 다만 부작용(side effect) 없이 새 테이블을 반환하기 때문에 함수형 스타일과 인라인 코드에 유리합니다.

3) FILTER operator

FILTER는 근본적으로 다른 물건입니다. 소스 테이블이 SORTED 또는 HASHED이거나 SECONDARY KEY가 정의되어 있어야만 동작하며, 필터 조건은 그 키의 필드만 사용할 수 있습니다. 반환 타입은 항상 STANDARD TABLE이며, 인덱스를 직접 활용하기 때문에 매우 큰 컬렉션에서 특정 키 값 그룹만 뽑아낼 때 압도적으로 빠릅니다. 조건 표현이 =, IN에 한정되고 복잡한 논리식은 지원하지 않는다는 제약이 있습니다.

비유하자면 LOOP는 서점 전체를 훑는 방식, FILTER는 이미 저자별로 정리된 카탈로그(secondary index)에서 곧바로 해당 서가로 이동하는 방식입니다.

실전 코드 1단계 — 기본 필터링 세 가지 방식

SalesOrder 테이블에서 특정 지역(region)의 주문만 뽑는 시나리오로 시작합니다. 세 방식을 나란히 비교해 봅니다.

TYPES: BEGIN OF ty_sales_order,
         order_id     TYPE n LENGTH 10,
         customer_id  TYPE n LENGTH 8,
         region       TYPE c LENGTH 3,
         net_amount   TYPE p LENGTH 13 DECIMALS 2,
         order_date   TYPE d,
       END OF ty_sales_order,
       tt_sales_order TYPE STANDARD TABLE OF ty_sales_order WITH EMPTY KEY.

DATA(lt_orders) = VALUE tt_sales_order(
    ( order_id = '1001' customer_id = '10' region = 'KR' net_amount = '15000.00' )
    ( order_id = '1002' customer_id = '11' region = 'JP' net_amount = '22000.00' )
    ( order_id = '1003' customer_id = '12' region = 'KR' net_amount = '8500.00'  )
    ( order_id = '1004' customer_id = '13' region = 'US' net_amount = '41000.00' )
    ( order_id = '1005' customer_id = '14' region = 'KR' net_amount = '17300.00' ) ).

" 방식 A: 전통적 LOOP AT ... WHERE
DATA lt_kr_a TYPE tt_sales_order.
LOOP AT lt_orders INTO DATA(ls_row) WHERE region = 'KR'.
  APPEND ls_row TO lt_kr_a.
ENDLOOP.

" 방식 B: FOR ... IN ... WHERE (함수형)
DATA(lt_kr_b) = VALUE tt_sales_order(
    FOR ls_o IN lt_orders WHERE ( region = 'KR' ) ( ls_o ) ).

" 방식 C: FILTER — sorted table 필요 (다음 단계에서 재구성)

실전 코드 2단계 — 로깅과 예외를 포함한 실무 시나리오

실제 서비스 코드는 예외 처리와 로깅이 붙습니다. Sorted table로 재정의해서 FILTER를 활용하고, 필터 결과가 비어 있을 때의 처리도 함께 넣습니다.

TYPES: tt_sales_order_sorted TYPE SORTED TABLE OF ty_sales_order
         WITH NON-UNIQUE KEY region customer_id.

CLASS lcl_order_service DEFINITION.
  PUBLIC SECTION.
    METHODS filter_by_region
      IMPORTING it_orders        TYPE tt_sales_order_sorted
                iv_region        TYPE c
      RETURNING VALUE(rt_result) TYPE tt_sales_order.
ENDCLASS.

CLASS lcl_order_service IMPLEMENTATION.
  METHOD filter_by_region.
    IF iv_region IS INITIAL.
      RETURN.
    ENDIF.

    " sorted table 이므로 FILTER 사용 가능 (primary key 접두: region)
    rt_result = FILTER #( it_orders WHERE region = iv_region ).

    IF rt_result IS INITIAL.
      MESSAGE |No orders found for region { iv_region }| TYPE 'S'.
    ELSE.
      cl_demo_output=>write( |Filtered { lines( rt_result ) } orders for { iv_region }| ).
    ENDIF.
  ENDMETHOD.
ENDCLASS.

여기서 중요한 포인트는 FILTER의 WHERE 절이 반드시 sorted table의 키 필드(region)만 사용해야 한다는 점입니다. 예컨대 WHERE net_amount > 10000이라고 쓰면 컴파일 에러가 납니다. FILTER는 인덱스를 활용하기 때문에 조건 자유도를 희생한 셈입니다.

실전 코드 3단계 — 프로덕션 성능 비교 벤치마크

10만 건 규모의 SalesOrder에서 세 방식을 실측 비교하는 벤치마크입니다.

DATA: lt_standard TYPE STANDARD TABLE OF ty_sales_order WITH EMPTY KEY,
      lt_sorted   TYPE SORTED TABLE OF ty_sales_order
                    WITH NON-UNIQUE KEY region,
      lv_start    TYPE i,
      lv_end      TYPE i.

DO 100000 TIMES.
  DATA(lv_idx) = sy-index.
  APPEND VALUE #(
    order_id    = lv_idx
    customer_id = lv_idx MOD 5000
    region      = SWITCH string( lv_idx MOD 4
                                 WHEN 0 THEN 'KR'
                                 WHEN 1 THEN 'JP'
                                 WHEN 2 THEN 'US'
                                 ELSE        'EU' )
    net_amount  = lv_idx * '1.5'
    order_date  = sy-datum ) TO lt_standard.
ENDDO.
lt_sorted = lt_standard.

" 벤치마크 A: LOOP AT ... WHERE
DATA lt_a TYPE STANDARD TABLE OF ty_sales_order WITH EMPTY KEY.
GET RUN TIME FIELD lv_start.
LOOP AT lt_standard INTO DATA(ls_a) WHERE region = 'KR'.
  APPEND ls_a TO lt_a.
ENDLOOP.
GET RUN TIME FIELD lv_end.
cl_demo_output=>write( |LOOP standard : { lv_end - lv_start } µs, { lines( lt_a ) } rows| ).

" 벤치마크 B: FOR IN WHERE
GET RUN TIME FIELD lv_start.
DATA(lt_b) = VALUE tt_sales_order( FOR o IN lt_standard WHERE ( region = 'KR' ) ( o ) ).
GET RUN TIME FIELD lv_end.
cl_demo_output=>write( |FOR IN WHERE  : { lv_end - lv_start } µs, { lines( lt_b ) } rows| ).

" 벤치마크 C: FILTER (sorted)
GET RUN TIME FIELD lv_start.
DATA(lt_c) = FILTER #( lt_sorted WHERE region = 'KR' ).
GET RUN TIME FIELD lv_end.
cl_demo_output=>write( |FILTER sorted : { lv_end - lv_start } µs, { lines( lt_c ) } rows| ).
방식10만 건 필터 시간 (참고치)주 특성
LOOP standard약 45ms풀 스캔, 단순
FOR IN WHERE약 47msLOOP와 유사, 인라인
FILTER sorted약 6ms이진 탐색 기반, 압도적

단, 결과 행 수가 전체의 절반을 넘어가면 FILTER의 이점은 급격히 줄어듭니다. 인덱스 접근으로 아낄 시간보다 결과 복사 비용이 더 커지기 때문입니다.

상황별 선택 기준 — 언제 무엇을 써야 하는가

세 방식을 고르는 판단 트리는 비교적 단순합니다.

  • 테이블이 standard table이고 조건이 복잡하다LOOP AT ... WHERE. 가장 자유롭고 유지보수 하기 쉽습니다.
  • 결과가 새로운 인라인 컬렉션이어야 하고, 코드 스타일이 함수형이다FOR ... IN ... WHERE. VALUE 컨스트럭터 안에서 쓸 수 있어 선언적 코드에 적합합니다.
  • sorted/hashed table이고, 필터 조건이 key 필드의 = / IN 이고, 건수가 크다FILTER. 10만 건 이상에서 5~10배 빠를 수 있습니다.
  • 부정 조건이 필요하다(NOT IN 개념)FILTER #( it EXCEPT WHERE ... ). EXCEPT 옵션을 활용합니다.

실무 팁: 소규모 데이터(수천 건 이하)에서는 세 방식 모두 체감 차이가 거의 없습니다. 성능이 중요해지는 임계점은 보통 5만 건 전후입니다. 그 아래에서는 코드 가독성과 유지보수성을 우선하세요.

흔한 실수와 트러블슈팅

FAQ 1. FILTER에 여러 조건을 AND로 걸 수 있나요? — 부분적으로 가능합니다. FILTER의 WHERE는 sorted 키를 왼쪽부터 연속으로 사용해야 하고, 각 필드는 = 또는 IN 조건만 허용됩니다. region = 'KR' AND customer_id = 10은 되지만, net_amount > 1000처럼 키가 아닌 필드나 범위 비교는 오류입니다.

FAQ 2. Sorted table에 secondary key도 필요할까요? — 필터 조건이 primary key와 다른 필드를 쓴다면 secondary sorted/hashed key를 추가하는 것이 정답입니다. WITH NON-UNIQUE SORTED KEY by_region COMPONENTS region 같은 방식으로 정의하고, FILTER #( it USING KEY by_region WHERE region = 'KR' )로 명시적으로 지정합니다. 지정하지 않으면 예상과 다른 키가 선택되어 성능이 무너지는 경우가 있습니다.

FAQ 3. FOR IN WHERE가 LOOP보다 느릴 때가 있던데 왜인가요? — 결과를 VALUE로 새 테이블에 담을 때 컨스트럭터가 매번 목적지 테이블을 초기화·확장하기 때문입니다. 반복 호출되는 함수 안에서 VALUE로 매번 컬렉션을 생성하면 오버헤드가 누적됩니다. 이런 코드는 미리 선언한 테이블에 APPEND하는 LOOP가 더 나을 수 있습니다.

그 외 자주 마주치는 함정으로는 (1) FILTER 결과 타입이 항상 standard table이라 sorted 결과를 기대하다가 타입 미스매치가 나는 경우, (2) EXCEPT 옵션(FILTER #( it EXCEPT WHERE ... ))을 몰라서 부정 조건을 억지로 짜는 경우, (3) hashed table에서 primary key 부분 조건으로 FILTER를 시도하다가 결국 풀 스캔으로 떨어지는 경우가 있습니다.

더 깊이 파고들 주제

필터링 성능을 더 짜내야 한다면 다음 주제들을 이어서 살펴보세요. 첫째, REDUCE 연산자와의 결합 — 필터 후 즉시 집계까지 한 표현식으로 처리하는 패턴입니다. 둘째, secondary key의 lazy update 특성과 언제 인덱스가 재계산되는지에 대한 이해입니다. 셋째, ABAP SQL의 FOR ALL ENTRIES와 CDS View의 필터 pushdown — 어차피 대량 데이터는 DB에서 걸러 오는 것이 정답인 경우가 많습니다. 마지막으로 ABAP Cloud의 RANGE TABLE 활용법도 함께 익히면 필터 표현이 한층 강력해집니다.

세 방식은 경쟁 관계가 아니라 서로 다른 목적에 최적화된 도구입니다. LOOP는 범용성, FOR IN WHERE는 표현력, FILTER는 성능. 코드 리뷰에서 "왜 이 방식을 골랐는가"에 세 줄로 답할 수 있다면, 이미 절반은 프로 ABAP 개발자입니다.

댓글 0

아직 댓글이 없습니다.