ABAP

내부 테이블 조회 속도 90% — SECONDARY KEY 3단계 #shorts #SAP #ABAP

▶ YouTube에서 보기

내부 테이블 선형 탐색의 성능 문제

대규모 트랜잭션을 다루는 ABAP 프로그램에서 가장 흔히 발견되는 병목은 의외로 데이터베이스 조회가 아니라 메모리 위에 올라온 내부 테이블의 비효율적인 탐색입니다. STANDARD TABLE은 기본적으로 인덱스가 없으므로 READ TABLE WITH KEY 또는 LOOP AT ... WHERE 구문은 첫 행부터 마지막 행까지 순차적으로 비교하는 선형 탐색(linear search)을 수행합니다. 100건 정도라면 체감되지 않지만 100만 건의 판매오더 항목을 다루는 배치 잡에서 또 다른 50만 건의 가격 조건 테이블을 키로 조회한다면 최악의 경우 5,000억 회의 비교 연산이 발생합니다.

전통적으로 이 문제는 SORTED TABLE 또는 HASHED TABLE로 테이블 종류 자체를 바꾸어 해결해 왔지만, 이는 이미 STANDARD TABLE로 정의된 시그니처를 가진 BAPI 인터페이스나 GLOBAL TYPE을 따르는 시나리오에서 적용이 어렵습니다. SAP는 ABAP 7.02부터 STANDARD TABLE을 유지하면서도 부가적인 정렬/해시 인덱스를 덧붙일 수 있는 SECONDARY KEY를 도입했습니다. 이 글에서는 SalesOrder/PurchaseOrder 시나리오를 통해 선형 탐색을 인덱스 탐색으로 전환하는 실전 패턴을 단계별로 풀어봅니다.

핵심 개념을 다지기 위한 사전 이해

이 글은 ABAP 7.40 SP08 이상 또는 ABAP for Cloud Development(ABAP Cloud) 환경을 전제로 합니다. 또한 다음 개념에 대한 기본 이해가 있다고 가정합니다. 내부 테이블 종류(STANDARD/SORTED/HASHED), 테이블 키와 PRIMARY KEY 선언, READ TABLE/LOOP AT 구문, ST05/SAT 트랜잭션을 통한 런타임 측정. 추가로 BAdI 또는 RAP behavior implementation에서 동일 데이터셋을 여러 키로 반복 접근하는 패턴을 본 적이 있다면 SECONDARY KEY의 가치가 더 빠르게 와닿을 것입니다.

환경과 권장 버전

SECONDARY KEY 자체는 NetWeaver 7.02부터 사용 가능하지만 LOOP AT ... USING KEY가 안정적으로 동작하고 옵티마이저가 자동으로 secondary key를 선택하는 기능은 7.40 SP08 이후가 권장됩니다. S/4HANA 2022, S/4HANA Cloud Public Edition, ABAP Environment(BTP) 모두에서 동일하게 사용할 수 있습니다. ABAP Cloud 환경에서는 ATC가 secondary key 사용을 권장하는 검사(SLIN_PERF) 규칙을 가지고 있으므로 클린 코어 마이그레이션 작업에도 직접적인 가치가 있습니다. 측정 도구는 SE30/SAT(Runtime Analysis)과 ABAP Profiler(ADT)를 사용하며, 본문에서는 GET RUN TIME FIELD를 활용한 간단한 마이크로 벤치마크로 차이를 비교합니다.

SECONDARY KEY 개념과 sorted/hashed 방식 비교

SECONDARY KEY는 STANDARD/SORTED/HASHED 어느 종류의 내부 테이블에도 추가로 붙일 수 있는 보조 인덱스입니다. 데이터베이스의 secondary index와 비슷한 비유로 이해하면 좋습니다. 기본 테이블 본문은 그대로 두고, 별도의 정렬 트리(red-black tree) 또는 해시 버킷(hash bucket)을 옆에 두어 특정 컬럼 조합으로 빠르게 위치를 찾도록 만드는 자료구조입니다.

  • SORTED secondary key: 이진 탐색 기반, O(log n) 복잡도. 범위(BETWEEN, <=) 조회와 정렬 순회 모두 지원.
  • HASHED secondary key: 해시 기반, 평균 O(1). 등가(=) 조회 전용이며 반드시 UNIQUE여야 함.
  • UNIQUE/NON-UNIQUE: SORTED는 두 모드 모두 지원, HASHED는 UNIQUE만 허용.

중요한 점은 secondary key가 lazy update를 지원한다는 것입니다. 즉 INSERT/MODIFY/DELETE가 발생해도 secondary key 인덱스는 즉시 갱신되지 않고, 해당 키를 처음 사용하는 시점에 일괄 재구축됩니다. 이를 "delayed update"라 부르며, 대량 적재 후 조회가 반복되는 패턴에서 특히 효율적입니다. 반대로 적재와 조회가 번갈아 일어나는 패턴에서는 매번 재구축 비용이 발생할 수 있어 주의가 필요합니다.

WITH NON-UNIQUE SORTED KEY 선언 문법

가장 일반적인 형태는 STANDARD TABLE 위에 NON-UNIQUE SORTED secondary key를 얹는 방식입니다. 판매오더 헤더를 다루는 시나리오를 가정해 보겠습니다.

TYPES: BEGIN OF ty_sales_order,
         so_id        TYPE c LENGTH 10,
         customer_id  TYPE c LENGTH 10,
         sales_org    TYPE c LENGTH 4,
         created_on   TYPE d,
         net_amount   TYPE p LENGTH 13 DECIMALS 2,
         currency     TYPE c LENGTH 3,
       END OF ty_sales_order.

DATA gt_sales_order TYPE STANDARD TABLE OF ty_sales_order
     WITH NON-UNIQUE DEFAULT KEY
     WITH NON-UNIQUE SORTED KEY by_customer
          COMPONENTS customer_id sales_org
     WITH NON-UNIQUE SORTED KEY by_date
          COMPONENTS created_on.

한 테이블에 최대 15개의 secondary key를 정의할 수 있습니다. 위 예제는 두 개의 보조 키 by_customer, by_date를 가지며, 각각 다른 액세스 패턴에 최적화되어 있습니다. WITH NON-UNIQUE DEFAULT KEY는 STANDARD TABLE의 기본(primary) 키를 명시한 부분이며, 이후 정의된 secondary key가 추가됩니다.

READ TABLE USING KEY — 단건 인덱스 조회

단일 행을 조회할 때는 USING KEY 구문으로 어떤 secondary key를 사용할지 명시적으로 지정합니다. 옵티마이저가 자동으로 적합한 키를 선택하기도 하지만, 명시적으로 지정하면 가독성과 예측 가능성이 높아집니다.

DATA(lv_start) = cl_abap_timer=>get_runtime( ).

" 1) 선형 탐색 (primary key가 customer_id를 포함하지 않음)
READ TABLE gt_sales_order
     WITH KEY customer_id = '1000000234'
              sales_org   = '1710'
     INTO DATA(ls_linear).

" 2) secondary sorted key를 통한 이진 탐색
READ TABLE gt_sales_order
     WITH KEY by_customer
              COMPONENTS customer_id = '1000000234'
                         sales_org   = '1710'
     INTO DATA(ls_indexed).

두 구문은 결과적으로 동일한 행을 찾지만, 100만 건 테이블에서 첫 번째는 평균 50만 회 비교, 두 번째는 약 20회(log₂ 1,000,000 ≈ 20)의 비교로 끝납니다. 실측에서는 일반적으로 100배에서 1,000배 이상의 차이가 납니다.

LOOP AT USING KEY — 반복 처리 성능 최적화

실무에서 더 큰 효과를 보는 곳은 nested loop 시나리오입니다. 외부 루프가 PurchaseOrder 헤더, 내부에서 동일 vendor의 SalesOrder를 찾는 전형적인 패턴을 봅시다.

DATA gt_purchase_order TYPE STANDARD TABLE OF ty_purchase_order
     WITH NON-UNIQUE DEFAULT KEY.

DATA gt_so_line TYPE STANDARD TABLE OF ty_so_line
     WITH NON-UNIQUE DEFAULT KEY
     WITH NON-UNIQUE SORTED KEY by_vendor
          COMPONENTS vendor_id plant.

LOOP AT gt_purchase_order INTO DATA(ls_po).

  " 키 컴포넌트 prefix가 일치하므로 secondary key 활용 가능
  LOOP AT gt_so_line USING KEY by_vendor
       INTO DATA(ls_so)
       WHERE vendor_id = ls_po-vendor_id
         AND plant     = ls_po-plant.

    " 매칭된 SO 라인에 대한 비즈니스 로직
    PERFORM reconcile_amounts USING ls_po ls_so.

  ENDLOOP.

ENDLOOP.

USING KEY by_vendor를 지정하면 LOOP가 정렬 인덱스의 해당 키 구간만 순회하므로, n × m → n × log m + matched_rows로 복잡도가 떨어집니다. WHERE 절의 첫 컴포넌트가 secondary key의 첫 컴포넌트와 일치해야 한다는 점이 중요한 제약입니다(좌측 prefix 규칙).

WITH UNIQUE HASHED KEY — 유일 키 해시 인덱스

등가(=) 조회만 발생하고 키 유일성이 보장된다면 HASHED secondary key가 가장 빠릅니다. 자재 마스터를 SalesOrder Line의 material_id로 lookup하는 시나리오를 예로 듭니다.

DATA gt_material TYPE STANDARD TABLE OF ty_material
     WITH NON-UNIQUE DEFAULT KEY
     WITH UNIQUE HASHED KEY by_matnr
          COMPONENTS material_id.

LOOP AT gt_so_line ASSIGNING FIELD-SYMBOL(<fs_so>).

  READ TABLE gt_material
       WITH KEY by_matnr
                COMPONENTS material_id = <fs_so>-material_id
       ASSIGNING FIELD-SYMBOL(<fs_mat>).

  IF sy-subrc = 0.
    <fs_so>-product_group = <fs_mat>-product_group.
    <fs_so>-base_uom      = <fs_mat>-base_uom.
  ELSE.
    " 결측 자재 처리 - 로깅 및 후속 단계 분기
    APPEND VALUE #( so_id    = <fs_so>-so_id
                    line_no  = <fs_so>-line_no
                    matnr    = <fs_so>-material_id
                    severity = 'W'
                    message  = |Material { <fs_so>-material_id } not found| )
           TO gt_log.
  ENDIF.

ENDLOOP.

HASHED 키는 평균 O(1) 접근을 보장하지만 정렬 순회가 불가능하다는 점, 그리고 메모리 오버헤드가 SORTED보다 약 20~40% 더 크다는 점을 감안해야 합니다. ASSIGNING FIELD-SYMBOL을 함께 사용해 불필요한 구조체 복사를 제거하는 것도 성능 측면에서 권장됩니다.

SECONDARY KEY 사용 시 주의사항 및 메모리 트레이드오프

secondary key는 만능 도구가 아닙니다. 다음 항목들은 도입 전 반드시 점검해야 합니다.

  • 메모리 비용: 각 secondary key는 추가 인덱스 메모리를 소비합니다. 100만 건, 키 길이 14바이트 기준 약 14~20MB가 키 하나당 더 필요합니다. work process 메모리 한도(ztta/roll_extension)와의 균형을 확인해야 합니다.
  • 지연 갱신 비용: 적재와 조회가 번갈아 일어나면 매번 인덱스 재구축이 트리거됩니다. 일반적으로 "한 번에 다 채우고 → 여러 번 조회" 패턴에서 효과가 큽니다.
  • 좌측 prefix 규칙: WHERE/WITH KEY에서 사용한 컴포넌트가 secondary key 정의의 앞부분과 일치해야 옵티마이저가 키를 선택합니다.
  • NON-UNIQUE 정렬 키와 INSERT 위치: STANDARD TABLE의 INSERT는 여전히 APPEND처럼 동작하며, secondary key는 별도 인덱스만 갱신합니다. 결과 순서를 SORTED TABLE처럼 기대하면 안 됩니다.

FAQ 1) "secondary key를 지정했는데 ST05에서 여전히 선형 탐색이 나옵니다."
대부분 WHERE 절의 컴포넌트 순서가 키 정의 앞부분과 일치하지 않거나, 옵티마이저가 자동 선택을 거부한 경우입니다. USING KEY로 명시 지정하거나 ATC SLIN_PERF 검사를 돌려 원인을 좁히는 것이 권장됩니다.

FAQ 2) "한 테이블에 secondary key를 몇 개까지 두는 게 안전한가요?"
문법상 15개까지지만, 실무에서는 3~5개를 넘어가면 메모리/유지보수 비용이 효익을 넘는 경우가 많습니다. 액세스 패턴을 측정하고 가장 빈번한 패턴 위주로 좁히는 것이 일반적입니다.

FAQ 3) "SORTED TABLE 대신 STANDARD + secondary sorted key를 쓰는 이유는?"
SORTED TABLE은 INSERT 위치가 키 순서로 강제되어 대량 적재 시 비용이 큽니다. 또한 기존 인터페이스가 STANDARD TABLE을 요구하면 시그니처를 바꿀 수 없습니다. secondary key는 이 두 제약을 우회하면서 조회 성능을 얻는 절충안입니다.

실전 적용 사례 — 대용량 판매오더 처리 비교

마지막으로 50만 건의 SalesOrder Line과 10만 건의 PricingCondition을 매칭하는 야간 배치 잡을 가정합니다. 기존 코드는 LOOP 안에서 STANDARD TABLE을 단순 WITH KEY로 조회해 약 47분이 걸렸습니다. 다음과 같이 PricingCondition 테이블에 hashed secondary key를 부여합니다.

DATA gt_pricing TYPE STANDARD TABLE OF ty_pricing_cond
     WITH NON-UNIQUE DEFAULT KEY
     WITH UNIQUE HASHED KEY by_combo
          COMPONENTS condition_type sales_org material_id.

DATA(lv_t0) = cl_abap_runtime=>create_hr_timer( )->get_runtime( ).

LOOP AT gt_so_line ASSIGNING FIELD-SYMBOL(<fs_line>).

  READ TABLE gt_pricing
       WITH KEY by_combo
                COMPONENTS condition_type = 'PR00'
                           sales_org      = <fs_line>-sales_org
                           material_id    = <fs_line>-material_id
       ASSIGNING FIELD-SYMBOL(<fs_pr>).

  IF sy-subrc = 0.
    <fs_line>-unit_price = <fs_pr>-amount.
    <fs_line>-net_amount = <fs_pr>-amount * <fs_line>-quantity.
  ENDIF.

ENDLOOP.

DATA(lv_t1) = cl_abap_runtime=>create_hr_timer( )->get_runtime( ).
WRITE: / |Elapsed (us): { lv_t1 - lv_t0 }|.

동일 데이터셋에서 위 코드는 약 38초로 단축되었습니다(약 74배 개선). 같은 패턴을 단위 테스트로 보호하기 위해서는 ABAP Unit에서 CL_ABAP_UNIT_ASSERT=>ASSERT_EQUALS와 함께 SETUP에서 대표 데이터를 적재하고, given-when-then 형식으로 키 컴포넌트 누락 시의 회귀를 잡는 것이 권장됩니다. 또한 ATC의 SLIN_PERF, code_pal_for_abap의 secondary_key check를 CI에 포함하면 향후 다른 개발자가 잘못된 키 사용 패턴을 도입하는 것을 사전 차단할 수 있습니다.

여기까지 적용했다면 RAP behavior implementation의 internal buffer에 secondary key를 적용하거나, LOOP AT GROUP BY와 결합한 집계 시나리오, 그리고 BTP ABAP Environment에서 released API와의 호환성 검사를 살펴보는 것이 자연스러운 흐름입니다. 관련 자료로는 help.sap.com의 ABAP Keyword Documentation 중 "itab - secondary_key" 항목, "Performance Notes for Internal Tables", "READ TABLE - key" 페이지, SAP Community의 "Internal table benchmarks" 블로그, code_pal_for_abap GitHub 저장소(secondary_key_check) 등이 권장 출발점입니다.

댓글 0

아직 댓글이 없습니다.