개요와 이 글에서 얻어갈 것
ABAP 7.40 이후 내부 테이블에서 부분집합을 뽑아내는 방법은 크게 세 갈래로 갈립니다. 표현식 기반의 FILTER 연산자, FOR ... IN ... WHERE ... 형태의 테이블 컴프리헨션, 그리고 전통적인 LOOP AT ... WHERE 반복문입니다. 셋 다 결과 집합은 유사하게 만들 수 있지만 동작 원리, 요구되는 테이블 속성(SORTED/HASHED/STANDARD), 성능 특성, 그리고 코드 가독성 측면에서 큰 차이가 있습니다. 이 글은 세 방식의 내부 동작을 뜯어보고, 실제 판매오더/구매요청 시나리오에서 어떤 상황에 어떤 문법을 골라야 하는지 정리합니다.
- FILTER의 두 가지 모드(단일 테이블 조건, 필터 테이블 조인)와 키 요구조건 파악
- FOR ... IN ... WHERE의 테이블 컴프리헨션 동작 방식과 메모리 특성 이해
- LOOP AT ... WHERE가 여전히 유리한 상황과 이유 정리
- 1만~100만 건 규모 데이터에서 실전 선택 기준 습득
- 흔한 함정(키 지정 누락, WHERE 조건 형변환 등) 회피
이 글을 읽기 전에 알고 있으면 좋은 것
ABAP 내부 테이블의 세 종류(STANDARD, SORTED, HASHED)와 각각의 키 접근 특성, 그리고 DATA(...) 인라인 선언과 VALUE, CORRESPONDING 등 생성자 표현식에 대한 기초 지식이 있으면 도움이 됩니다. NetWeaver 7.40 SP08 이상 또는 ABAP Platform 2022 이후 릴리스를 전제로 설명합니다.
환경과 준비물
이 글의 예제는 다음 환경을 기준으로 검증했습니다. 릴리스마다 문법 미묘한 차이가 있어 개발 시스템 릴리스를 먼저 확인하는 것을 권장합니다.
- ABAP Platform 2023 (온프레미스) 또는 ABAP Cloud 2024 이후
- SAP NetWeaver AS ABAP 7.55 이상에서 대부분 동일하게 동작
- ADT(ABAP Development Tools) 3.36 이상 - Eclipse 기반 편집기
- 테스트용 임시 테이블:
zsales_order_t,zpo_item_t형태의 구조체 - 성능 측정:
GET RUN TIME FIELD또는 SAT 트랜잭션
ABAP Cloud(RAP) 환경에서는 클래식 문법 일부(예: WHERE 다이나믹 문자열)가 제약을 받으므로, 예제는 두 환경에서 모두 동작하는 문법 위주로 구성했습니다.
세 가지 필터링 방식의 뼈대와 동작 원리
세 방식을 한 문장으로 요약하면 다음과 같습니다.
FILTER는 표현식(expression) - 원본 테이블을 스캔하여 조건에 맞는 행만 담은 새 테이블을 즉시 반환합니다. FOR ... IN ... WHERE는 이터레이터(iterator) - 컴프리헨션 문법으로 새 테이블을 구성하되 각 요소를 어떻게 매핑할지 지정할 수 있습니다. LOOP AT ... WHERE는 명령형(imperative) - 한 행씩 순회하며 절차적 로직을 수행합니다.
비유하자면 FILTER는 냉장고 문을 열고 "유통기한 지난 것 다 꺼내"라고 통째로 지시하는 것이고, FOR ... IN ... WHERE는 "유통기한 지난 것을 꺼내되 라벨을 붙여서 새 바구니에 담아"라고 부가 작업을 지정하는 것이며, LOOP는 "하나씩 꺼내서 유통기한을 확인한 뒤 처리해"라고 순차적으로 지시하는 것에 가깝습니다.
중요한 제약: FILTER는 원본 테이블에 조건 필드에 대한 키가 정의되어 있어야 합니다. STANDARD 테이블에 표준키만 있으면 사용할 수 없고, SORTED/HASHED이거나 SECONDARY KEY가 있어야 합니다. 반면 FOR ... IN ... WHERE와 LOOP ... WHERE는 STANDARD 테이블에서도 자유롭게 씁니다. 이 차이가 실전에서 선택을 좌우하는 핵심 포인트입니다.
또 하나 - FILTER는 두 가지 모드가 있습니다. (1) 단일 테이블 조건 모드: FILTER #( itab WHERE fld = value ), (2) 필터 테이블 모드: FILTER #( itab IN filter_tab WHERE fld = filter_tab-fld ). 두 번째 모드는 조인 성격을 띠어서 "다른 테이블에 존재하는 키만 남기기" 같은 세미조인 시나리오에 강력합니다.
1단계: 기본 예제 - 세 문법을 나란히 비교
판매오더 라인 아이템 중 금액이 1000 이상인 행만 골라내는 시나리오입니다. 같은 결과를 세 방식으로 구현해 봅니다.
TYPES: BEGIN OF ty_so_item,
so_number TYPE c LENGTH 10,
item_no TYPE i,
material TYPE c LENGTH 18,
net_amount TYPE p LENGTH 9 DECIMALS 2,
currency TYPE c LENGTH 5,
END OF ty_so_item.
" FILTER를 쓰려면 net_amount에 대한 키가 필요 - SORTED로 선언
DATA lt_so_items TYPE SORTED TABLE OF ty_so_item
WITH NON-UNIQUE KEY net_amount.
lt_so_items = VALUE #(
( so_number = '4500001001' item_no = 10 material = 'MAT-A' net_amount = 850 currency = 'EUR' )
( so_number = '4500001001' item_no = 20 material = 'MAT-B' net_amount = 1450 currency = 'EUR' )
( so_number = '4500001002' item_no = 10 material = 'MAT-C' net_amount = 2200 currency = 'EUR' )
( so_number = '4500001003' item_no = 10 material = 'MAT-D' net_amount = 300 currency = 'EUR' )
).
" 방식 1) FILTER - 한 줄로 부분집합 추출
DATA(lt_hi_value_filter) = FILTER #( lt_so_items WHERE net_amount >= 1000 ).
" 방식 2) FOR ... IN ... WHERE (테이블 컴프리헨션)
DATA(lt_hi_value_for) = VALUE ty_so_item_tt(
FOR <ls> IN lt_so_items WHERE ( net_amount >= 1000 )
( <ls> )
).
" 방식 3) LOOP AT ... WHERE (전통적 방식)
DATA lt_hi_value_loop TYPE STANDARD TABLE OF ty_so_item.
LOOP AT lt_so_items ASSIGNING FIELD-SYMBOL(<fs>) WHERE net_amount >= 1000.
APPEND <fs> TO lt_hi_value_loop.
ENDLOOP.
세 방식 모두 아이템 20과 SO 4500001002 라인을 반환합니다. FILTER는 표현식이라 DATA(...)로 즉시 타입 추론이 되고, FOR 컴프리헨션은 결과 테이블 타입을 명시해야 합니다. LOOP는 사전에 결과 테이블을 선언해야 합니다.
2단계: 실무 시나리오 - 필터 테이블 조인과 에러 처리
실무에서 자주 마주치는 시나리오는 "다른 리스트에 있는 키만 남기기"입니다. 예를 들어, 특정 우선순위 자재 목록(lt_priority_mat)에 포함된 판매오더 라인만 추출하는 경우입니다. LOOP로 구현하면 이중 순회가 필요하지만 FILTER는 필터 테이블 모드로 우아하게 처리합니다.
TYPES: BEGIN OF ty_priority_mat,
material TYPE c LENGTH 18,
END OF ty_priority_mat.
DATA lt_priority_mat TYPE HASHED TABLE OF ty_priority_mat
WITH UNIQUE KEY material.
lt_priority_mat = VALUE #(
( material = 'MAT-A' )
( material = 'MAT-C' )
).
" FILTER 필터 테이블 모드 - material 필드에 세컨더리 키 필요
DATA lt_orders TYPE STANDARD TABLE OF ty_so_item
WITH NON-UNIQUE SORTED KEY sk_mat COMPONENTS material.
" ... lt_orders 채우기 생략 ...
TRY.
DATA(lt_priority_orders) = FILTER #(
lt_orders USING KEY sk_mat
IN lt_priority_mat WHERE material = lt_priority_mat-material
).
IF lt_priority_orders IS INITIAL.
MESSAGE |우선순위 자재에 해당하는 오더가 없습니다| TYPE 'I'.
ENDIF.
CATCH cx_sy_itab_error INTO DATA(lx_err).
" 로그 프레임워크에 기록 (예: cl_bali_log)
cl_demo_output=>write( |FILTER 실패: { lx_err->get_text( ) }| ).
ENDTRY.
같은 시나리오를 LOOP로 구현하면 다음과 같습니다. HASHED 테이블에 READ TABLE ... WITH TABLE KEY로 존재 여부를 체크하는 패턴이 표준입니다.
DATA lt_priority_orders_loop TYPE STANDARD TABLE OF ty_so_item.
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<order>).
READ TABLE lt_priority_mat WITH TABLE KEY material = <order>-material
TRANSPORTING NO FIELDS.
IF sy-subrc = 0.
APPEND <order> TO lt_priority_orders_loop.
ENDIF.
ENDLOOP.
두 코드 모두 O(n) 수준으로 동작하지만(HASHED 조회 O(1) 덕분), FILTER 쪽이 6줄 vs 8줄로 조금 짧고 의도가 더 명확합니다. 다만 FILTER는 예외 상황에서 원본 테이블 키가 없으면 컴파일 타임에 문법 오류가 나므로 에러 처리보다는 설계 단계에서 테이블 키를 미리 설계하는 것이 중요합니다.
3단계: 프로덕션 - 성능 벤치마크와 최적 선택 기준
실제로 세 방식의 성능을 어떻게 다를까요. 10만 건 판매오더 라인에서 조건에 맞는 5% 정도를 뽑아내는 벤치마크 코드입니다. 이런 성능 측정은 캐시, GC, 시스템 부하 등에 영향을 받으므로 반드시 여러 번 반복하고 평균을 내야 합니다.
CLASS zcl_filter_bench DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS run_benchmark
IMPORTING iv_rows TYPE i DEFAULT 100000
EXPORTING ev_ms_filter TYPE i
ev_ms_for TYPE i
ev_ms_loop TYPE i.
ENDCLASS.
CLASS zcl_filter_bench IMPLEMENTATION.
METHOD run_benchmark.
DATA lt_data TYPE SORTED TABLE OF ty_so_item
WITH NON-UNIQUE KEY net_amount.
DATA lv_t1 TYPE i.
DATA lv_t2 TYPE i.
" 테스트 데이터 생성
DO iv_rows TIMES.
APPEND VALUE #( so_number = |{ sy-index ALPHA = IN }|
item_no = sy-index
material = |MAT-{ sy-index MOD 500 }|
net_amount = ( sy-index MOD 5000 ) + 1
currency = 'EUR' ) TO lt_data.
ENDDO.
" FILTER 측정
GET RUN TIME FIELD lv_t1.
DATA(lt_r1) = FILTER #( lt_data WHERE net_amount >= 4500 ).
GET RUN TIME FIELD lv_t2.
ev_ms_filter = ( lv_t2 - lv_t1 ) / 1000.
" FOR 컴프리헨션 측정
GET RUN TIME FIELD lv_t1.
DATA(lt_r2) = VALUE ty_so_item_tt(
FOR <s> IN lt_data WHERE ( net_amount >= 4500 ) ( <s> ) ).
GET RUN TIME FIELD lv_t2.
ev_ms_for = ( lv_t2 - lv_t1 ) / 1000.
" LOOP 측정
DATA lt_r3 TYPE STANDARD TABLE OF ty_so_item.
GET RUN TIME FIELD lv_t1.
LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<row>) WHERE net_amount >= 4500.
APPEND <row> TO lt_r3.
ENDLOOP.
GET RUN TIME FIELD lv_t2.
ev_ms_loop = ( lv_t2 - lv_t1 ) / 1000.
ENDMETHOD.
ENDCLASS.
일반적으로 다음 경향이 관찰됩니다(측정 결과는 시스템별로 다를 수 있음).
- SORTED/HASHED 키가 조건 필드와 일치: FILTER와 LOOP WHERE 모두 유사하게 빠름. 인덱스/해시 활용으로 O(log n) 또는 O(1) 조회
- 조건 필드가 키가 아닌 STANDARD 테이블: FILTER는 사용 불가. LOOP WHERE와 FOR ... WHERE는 O(n) 전체 스캔
- 결과 집합을 그대로 반환: FILTER가 가장 간결하고 컴파일러 최적화에 유리
- 필터링과 동시에 매핑(필드 재계산): FOR ... IN ... WHERE가 최적. 예:
( amount = <s>-net_amount * 1.1 ) - 필터링 중 사이드 이펙트(외부 호출, 로깅 등): LOOP만 가능. FILTER/FOR는 표현식이라 문장 실행 불가
정리하면 선택 기준은 다음과 같습니다. (1) 순수 부분집합이면 FILTER, (2) 매핑이 섞이면 FOR, (3) 부수 효과나 복잡한 흐름 제어가 필요하면 LOOP를 씁니다.
흔한 실수와 트러블슈팅
Q1. FILTER 사용 시 "Component ... is not a table key" 에러가 납니다.
FILTER는 조건 필드가 기본 키 또는 세컨더리 키의 일부여야 합니다. 해결책은 두 가지 - (a) 테이블을 SORTED/HASHED로 재설계, (b) SECONDARY KEY 추가 후 USING KEY로 명시. 임시 목적이라면 세컨더리 키가 유지비용도 발생시키므로 그냥 LOOP WHERE를 쓰는 게 낫습니다.
Q2. FOR ... IN ... WHERE의 WHERE 조건에 괄호가 필요한지 헷갈립니다.
릴리스에 따라 WHERE 뒤에 논리 표현식을 괄호로 감싸는 것이 필수입니다(7.50 이후 명확). WHERE ( net_amount >= 1000 AND currency = 'EUR' ) 형태로 쓰세요. 반면 LOOP WHERE는 괄호 없이 WHERE net_amount >= 1000으로 쓰는 것이 관례입니다. 문법이 미묘하게 다르니 주의합니다.
Q3. FILTER로 필터링한 후 원본 테이블을 수정하면 결과도 바뀌나요?
아닙니다. FILTER는 결과를 새 테이블로 복사하여 반환합니다. 참조 공유가 아니므로 원본 수정과 무관합니다. 다만 대용량 데이터에서는 이 복사 비용이 무시할 수 없으므로, 단순히 순회만 할 목적이라면 LOOP WHERE로 원본을 직접 도는 것이 메모리 효율적입니다.
Q4. FOR ... IN ... WHERE 안에서 CATCH나 로깅을 하고 싶습니다.
불가능합니다. 표현식이라 문장을 넣을 수 없습니다. 예외 처리나 사이드 이펙트가 필요하면 LOOP로 전환하거나, 필터링과 처리를 분리하세요 - 먼저 FILTER로 대상만 뽑고, 그 결과를 LOOP로 처리합니다.
Q5. STANDARD 테이블에 FILTER를 쓸 방법이 정말 없나요?
표준 키만 있는 STANDARD 테이블은 FILTER 불가입니다. 그러나 SECONDARY KEY를 추가하면 사용 가능합니다. 예: DATA lt TYPE STANDARD TABLE OF ty_x WITH EMPTY KEY WITH NON-UNIQUE SORTED KEY sk_amt COMPONENTS net_amount. 이후 FILTER #( lt USING KEY sk_amt WHERE net_amount >= 1000 ) 형태로 씁니다.
이어서 살펴보면 좋은 주제
이 글에서 다룬 세 방식을 익혔다면, 다음으로는 (1) REDUCE 연산자로 필터링과 집계를 한 번에 처리하는 패턴, (2) CDS View의 WHERE 절과 ABAP 필터링의 역할 분담(가능하면 DB에 위임하는 원칙), (3) SQL로 처리 가능한 로직을 내부 테이블 필터링으로 끌어오지 않는 Code Pushdown 원칙, (4) LOOP AT GROUP BY를 활용한 그룹화 후 필터링, (5) ABAP Cloud RAP 환경에서의 필터링 제약 사항 순으로 확장하는 것을 권장합니다.
더 파고들 자료
- ABAP Keyword Documentation - Table Filtering with FILTER
- ABAP Keyword Documentation - Iteration Expressions with FOR
- ABAP Keyword Documentation - LOOP AT itab
- SAP Help Portal - ABAP Cloud Development Guide
- SAP Help - ABAP Platform Documentation
- SAP Community Blogs - ABAP Internal Tables 태그
- SAP Community - ABAP Q&A
댓글 0
아직 댓글이 없습니다.