ABAP

아직도 LOOP WHERE로 필터링? FILTER 3가지 이유 #shorts #SAP #ABAP

▶ YouTube에서 보기

FILTER란 무엇인가 — 핵심 개념과 등장 배경

ABAP 7.40 SP08부터 도입된 FILTER 연산자는 내부 테이블에서 특정 조건을 만족하는 행만 골라내어 새로운 내부 테이블로 반환하는 표현식 기반 필터링 도구입니다. 기존에는 LOOP AT ... WHERE로 한 줄씩 순회하며 APPEND하는 절차적 방식이 일반적이었지만, FILTER는 한 줄의 선언형 표현식으로 동일한 결과를 얻을 수 있도록 설계되었습니다.

이 표현식이 등장한 배경에는 두 가지 흐름이 있습니다. 첫째, ABAP이 함수형/선언형 스타일을 도입하면서 임시 변수를 줄이고 가독성을 높이는 방향으로 진화하고 있다는 점입니다. 둘째, 내부적으로 정렬된 키 또는 해시 키를 활용해 검색 비용을 줄이는 최적화가 가능하기 때문에, 단순 LOOP보다 성능상 유리한 경우가 많다는 점입니다.

한 마디로 정리하면, FILTER는 "키가 정의된 내부 테이블에서 조건에 맞는 행만 빠르게 추출"하는 전용 연산자입니다. 기존의 LOOP-APPEND 패턴을 한 줄로 압축하면서, 키 기반 접근으로 성능까지 챙기는 것이 핵심입니다.

LOOP AT ... WHERE의 한계와 성능 이슈

가장 익숙한 패턴인 LOOP AT itab WHERE ... APPEND ... TO result는 직관적이지만 몇 가지 약점이 있습니다.

  • 임시 변수 증가: 결과 테이블, 작업 영역(work area), 인덱스 변수가 늘어나 코드가 장황해집니다.
  • 전체 스캔 가능성: STANDARD TABLE의 경우 WHERE 절이 키와 무관하면 풀 스캔이 발생합니다.
  • 가독성 저하: "무엇을 원하는가"보다 "어떻게 모으는가"가 코드의 대부분을 차지합니다.
  • 인라인 사용 불가: 메서드 인자나 RETURNING 절에 바로 결과를 넘길 수 없습니다.
" 기존 LOOP AT WHERE 방식 - 5줄이 필요합니다
DATA: lt_open_orders TYPE STANDARD TABLE OF ty_sales_order,
      ls_order       TYPE ty_sales_order.

LOOP AT gt_sales_orders INTO ls_order WHERE status = 'OPEN'.
  APPEND ls_order TO lt_open_orders.
ENDLOOP.

위 코드는 동작은 명확하지만, "OPEN 상태인 주문만 골라내고 싶다"는 의도가 절차 속에 묻혀버립니다. 또한 STANDARD TABLE을 그대로 사용하면 키 기반 가속이 불가능하므로, 데이터가 수만 건 이상일 때 응답시간이 선형적으로 증가합니다.

FILTER 기본 문법과 내부 동작 원리

FILTER의 기본 구문은 다음과 같습니다.

result = FILTER #( source_itab [EXCEPT] [IN filter_itab] [USING KEY key_name]
                   WHERE col1 op value1 [AND|OR col2 op value2 ...] ).

핵심 포인트는 소스 내부 테이블이 반드시 정렬 키(SORTED) 또는 해시 키(HASHED)를 가지고 있어야 한다는 점입니다. 일반 STANDARD TABLE에 FILTER를 적용하려면 보조 키(secondary key)를 정의하거나, 변환 후 사용해야 합니다. 이 제약 자체가 FILTER 성능의 비밀이기도 합니다 — 키를 활용해 O(log n) 또는 O(1)로 후보 행에 접근하기 때문입니다.

FILTER에는 두 가지 모드가 있습니다.

  1. 조건식 모드 (WHERE 절 사용): 단일 소스 테이블에서 비교 연산으로 행을 거릅니다.
  2. 필터 테이블 모드 (IN 절 사용): 별도의 "필터 테이블"에 들어있는 키 값들을 기준으로 매칭/제외합니다.

단순 조건 필터링 실전 예제

먼저 판매 주문 데이터에서 특정 통화의 주문만 추출하는 예제를 보겠습니다. SORTED TABLE로 선언해 FILTER가 정상 동작하도록 합니다.

TYPES: BEGIN OF ty_sales_order,
         order_id   TYPE c LENGTH 10,
         customer   TYPE c LENGTH 8,
         currency   TYPE c LENGTH 3,
         amount     TYPE p LENGTH 9 DECIMALS 2,
         status     TYPE c LENGTH 4,
       END OF ty_sales_order.

DATA gt_orders TYPE SORTED TABLE OF ty_sales_order
               WITH NON-UNIQUE KEY currency.

" 샘플 데이터 적재
gt_orders = VALUE #(
  ( order_id = 'SO0001' customer = 'C100' currency = 'EUR' amount = '1200.00' status = 'OPEN' )
  ( order_id = 'SO0002' customer = 'C101' currency = 'USD' amount = '850.00'  status = 'CLSD' )
  ( order_id = 'SO0003' customer = 'C102' currency = 'EUR' amount = '430.00'  status = 'OPEN' )
  ( order_id = 'SO0004' customer = 'C103' currency = 'KRW' amount = '99000'   status = 'OPEN' )
).

" EUR 통화 주문만 한 줄로 추출
DATA(lt_eur_orders) = FILTER #( gt_orders WHERE currency = 'EUR' ).

cl_demo_output=>display( lt_eur_orders ).

이 표현식은 변수 선언과 LOOP가 모두 사라지고, 데이터 흐름이 직선적으로 읽힙니다. 또한 키가 currency이므로 내부적으로 이진 검색에 가까운 효율로 동작합니다.

복합 조건 AND/OR로 정교한 필터링하기

WHERE 절에는 일반적인 비교 연산자(=, <>, <, >, <=, >=)와 논리 연산자(AND, OR)를 자유롭게 조합할 수 있습니다. 단, 모든 비교 컬럼은 FILTER가 사용할 키의 일부거나, USING KEY로 지정된 보조 키의 컴포넌트여야 한다는 점이 중요합니다.

" 보조 키를 가진 SORTED 테이블 정의
DATA gt_purchase_items TYPE SORTED TABLE OF ty_purchase_item
     WITH NON-UNIQUE KEY plant
     WITH NON-UNIQUE SORTED KEY by_status_amount
          COMPONENTS status amount.

" plant = 'P100'이면서 금액이 500 이상인 항목만 추출
DATA(lt_high_value) = FILTER #( gt_purchase_items
                                WHERE plant = 'P100'
                                  AND amount >= 500 ).

" 보조 키를 명시적으로 지정해 status + amount 조건으로 필터링
DATA(lt_pending_big) = FILTER #( gt_purchase_items
                                 USING KEY by_status_amount
                                 WHERE status = 'PEND'
                                   AND amount > 1000 ).

실무에서 자주 놓치는 부분은 USING KEY 지정입니다. 보조 키를 정의해놓고도 USING KEY를 생략하면 기본 키 기준으로 동작해 의도와 다른 결과가 나옵니다. 또한 키에 포함되지 않은 컬럼을 WHERE에 사용하면 문법 오류가 발생하니, 키 설계가 곧 FILTER 설계라는 점을 기억해야 합니다.

EXCEPT 키워드로 역방향 필터링하기

때로는 "조건을 만족하지 않는" 행을 추출하고 싶을 때가 있습니다. EXCEPT 키워드는 WHERE 절의 결과를 뒤집어 조건에 해당하지 않는 행을 반환합니다.

" CLSD(종결) 상태가 아닌 모든 주문 추출 = 활성 주문 목록
DATA(lt_active_orders) = FILTER #( gt_orders
                                   EXCEPT
                                   WHERE status = 'CLSD' ).

" 필터 테이블 모드 + EXCEPT 조합 - 블랙리스트 고객 제외
DATA lt_blacklist TYPE SORTED TABLE OF ty_customer_key
     WITH UNIQUE KEY customer.

lt_blacklist = VALUE #( ( customer = 'C103' )
                        ( customer = 'C107' ) ).

DATA(lt_valid_orders) = FILTER #( gt_orders IN lt_blacklist
                                  EXCEPT
                                  USING KEY primary_key
                                  WHERE customer = lt_blacklist-customer ).

두 번째 예제처럼 IN과 EXCEPT를 조합하면 "특정 키 집합에 속하지 않는 행"이라는 표현이 자연스럽게 만들어집니다. 이는 SQL의 NOT IN과 의미가 유사하지만, 데이터베이스를 거치지 않고 내부 메모리에서 처리된다는 점이 다릅니다.

중첩 내부 테이블 필터링과 FOR 표현식 결합

실무에서는 헤더-아이템 구조처럼 내부 테이블이 또 다른 내부 테이블을 컴포넌트로 가지는 경우가 흔합니다. 이때 FILTER를 FOR 표현식, VALUE 연산자와 함께 쓰면 한 번의 표현식으로 깊이 있는 변환이 가능합니다.

TYPES: BEGIN OF ty_order_with_items,
         order_id TYPE c LENGTH 10,
         customer TYPE c LENGTH 8,
         items    TYPE SORTED TABLE OF ty_order_item
                  WITH NON-UNIQUE KEY material,
       END OF ty_order_with_items.

DATA gt_orders_full TYPE STANDARD TABLE OF ty_order_with_items.

" 각 주문 안에서 특정 자재('MAT-A')만 남긴 새 구조 만들기
DATA(lt_filtered) = VALUE ty_order_table(
  FOR <order> IN gt_orders_full
  ( order_id = <order>-order_id
    customer = <order>-customer
    items    = FILTER #( <order>-items WHERE material = 'MAT-A' ) )
).

이 패턴은 BAPI 호출 전 헤더-아이템을 가공하거나, RAP BO에서 자식 엔티티의 일부만 화면에 반환할 때 유용합니다. 절차적으로 작성하면 이중 LOOP에 INSERT가 섞여 50줄을 넘기는 로직이 6~7줄로 압축됩니다.

흔한 실수와 트러블슈팅 FAQ

Q1. "Internal table is not sorted or hashed" 오류가 떠요.
가장 흔한 원인입니다. FILTER는 STANDARD TABLE의 기본 키에 직접 적용할 수 없습니다. 해결책은 (1) SORTED/HASHED로 선언하거나, (2) WITH NON-UNIQUE SORTED KEY ... COMPONENTS ...로 보조 키를 추가한 뒤 USING KEY로 지정하는 것입니다.

Q2. WHERE 절에서 일반 변수를 비교할 수 있나요?
가능합니다. 다만 비교 대상 컬럼은 키 컴포넌트여야 하고, 우변에는 리터럴, 변수, 그리고 IN 모드인 경우 필터 테이블의 컴포넌트(filter_itab-col)를 쓸 수 있습니다. 복잡한 함수 호출이나 SELECT 서브쿼리는 사용할 수 없습니다.

Q3. 데이터가 10만 건인데 FILTER가 LOOP보다 느려요.
키 설계가 잘못되었거나, USING KEY 지정을 빠뜨렸을 가능성이 큽니다. ST05/SAT 트레이스로 어떤 키가 사용되는지 확인하세요. 또한 결과 테이블이 매우 클 경우 FILTER가 메모리 복사 비용을 그대로 떠안기 때문에, "정말 별도 테이블이 필요한가"를 먼저 검토하는 것이 좋습니다. 단순 순회만 필요하다면 LOOP가 더 가벼울 수 있습니다.

Q4. FILTER 결과를 다시 수정할 수 있나요?
FILTER는 새 내부 테이블을 만들어 반환하므로, 원본은 영향을 받지 않습니다. 결과를 수정해도 원본 테이블에는 반영되지 않습니다. 원본을 직접 변경해야 한다면 DELETE itab WHERE ... 같은 명령을 사용해야 합니다.

실무 적용 시 주의사항과 성능 최적화 포인트

  • 키 설계가 핵심: 자주 필터링되는 컬럼 조합을 보조 키로 설계하면 일관된 성능을 얻습니다. 너무 많은 보조 키는 INSERT/MODIFY 비용을 증가시키니 균형이 필요합니다.
  • 인라인 선언 활용: DATA(lt_x) = FILTER #( ... )로 결과 타입을 자동 추론하면 타입 선언이 줄어들고, 리팩터링 시 유지보수성이 좋아집니다.
  • 단위 테스트와의 궁합: FILTER는 부작용 없는 순수 표현식이므로 ABAP Unit에서 어설션 대상으로 삼기 좋습니다. 입력 itab과 기대 결과 itab만 준비하면 됩니다.
  • RAP/CDS와의 위치 선정: 대량 데이터는 가능하면 CDS 뷰나 SQL WHERE 절에서 먼저 거르고, 메모리에 올라온 뒤의 후처리에 FILTER를 활용하는 것이 권장됩니다. "DB에서 최대한 줄이고, ABAP 메모리에서는 FILTER로 다듬는다"는 분업이 일반적으로 효율적입니다.

한 걸음 더 나아가기 — 관련 표현식과 학습 경로

FILTER에 익숙해졌다면 다음 주제로 넘어가 보길 권장합니다. REDUCE는 필터링된 결과를 집계로 변환할 때 자연스럽게 이어지고, FOR ... WHERE를 활용한 VALUE 표현식은 FILTER와 비슷하지만 컬럼 변환을 함께 수행할 수 있어 더 유연합니다. CORRESPONDING 연산자는 FILTER 결과를 다른 구조의 테이블로 매핑할 때 짝을 이룹니다. 또한 ABAP Cloud 환경에서 동작 차이를 확인해 보면 RAP 핸들러 구현 시 어떤 표현식이 가장 적합한지 감이 잡힙니다.

장기적으로는 CDS View Entity의 WHERE 절, Open SQL의 신규 표현식, 그리고 ABAP SQL의 FOR ALL ENTRIES 대체 패턴까지 함께 익히면 데이터 처리 전반의 성능 감각이 단단해집니다.

더 깊이 파고들고 싶다면

버전 참고: 본문 내용은 ABAP 7.40 SP08 이상, ABAP Platform 2022 및 ABAP Cloud(Steampunk) 환경에서 일관되게 적용됩니다. 일부 보조 키 동작은 7.50 이상에서 최적화가 강화되었으므로, 사용 중인 NetWeaver/AS ABAP 릴리스를 확인하고 적용하시기 바랍니다.

댓글 0

아직 댓글이 없습니다.