1. SY-TABIX 의존의 문제점
오래된 ABAP 코드를 들여다보면 거의 모든 내부 테이블 조작이 SY-TABIX와 SY-SUBRC를 중심으로 돌아간다. 예를 들어 READ TABLE ... TRANSPORTING NO FIELDS로 인덱스를 확보한 뒤 그 값을 즉시 다른 명령에 넘기는 방식이다. 이 패턴은 동작은 하지만 두 가지 약점이 있다. 첫째, SY-TABIX는 부수효과(side-effect)에 의존하는 전역 상태라서 중간에 다른 ABAP 문(예: MODIFY, LOOP, CALL FUNCTION)이 끼어들면 값이 덮어써질 수 있다. 둘째, "값이 있는지"와 "어디 있는지"를 묻기 위해 같은 READ TABLE을 두 번 호출하는 중복 코드가 자주 발생한다.
특히 SalesOrder 라인 아이템을 다루는 로직에서 SY-SUBRC = 0 체크를 잊고 SY-TABIX를 곧바로 사용하면 직전에 실행된 다른 명령의 TABIX 값을 그대로 끌어 쓰는 버그가 흔하다. 운영 환경에서 발견되는 "엉뚱한 라인이 수정되는" 결함의 상당 부분이 이런 흐름에서 비롯된다. 이 글은 이런 부수효과 의존을 줄이고, 표현식 기반의 명시적 코드로 옮겨가는 방법을 다룬다.
2. LINE_INDEX와 LINE_EXISTS 개요
ABAP 7.4 이후 도입된 두 가지 내장 함수 line_exists( )와 line_index( )는 내부 테이블에 대한 질의를 표현식 형태로 만들어 준다. 둘 다 인자로 테이블 표현식(table expression)을 받는데, 이 표현식 자체는 데이터를 반환하지 않고 "조건이 평가될 위치"를 기술한다. 예를 들어 lt_items[ posnr = '0010' ]는 "posnr이 0010인 첫 라인"을 가리키는 표현식이다.
line_exists( lt_items[ posnr = '0010' ] )— 해당 라인의 존재 여부를abap_bool로 반환한다. 라인이 없어도 예외를 던지지 않는다.line_index( lt_items[ posnr = '0010' ] )— 일치하는 첫 라인의 인덱스를i타입으로 반환한다. 일치 라인이 없으면 0을 돌려준다.
핵심은 두 함수가 SY-SUBRC나 SY-TABIX를 변경하지 않는다는 점이다. 호출 전후 시스템 필드가 보존되므로, "확인 후 사용" 흐름을 한 줄에 표현해도 다른 명령의 부수효과를 침해하지 않는다. 비유하자면 SY-TABIX는 마지막에 다녀간 손님이 남긴 메모지이고, line_index는 그 자리에서 즉시 받아드는 영수증이다.
3. LOOP 안에서 LINE_INDEX 기본 활용
첫 예제는 PurchaseOrder 헤더 테이블과 라인 아이템 테이블을 연관 짓는 시나리오다. 헤더를 순회하면서 라인 테이블에서 첫 등장 인덱스를 기록하고 싶다고 하자. 전통적 방식은 READ TABLE ... TRANSPORTING NO FIELDS 후 SY-TABIX를 사용하는 것이지만, line_index를 쓰면 다음과 같이 깔끔하다.
DATA: lt_po_header TYPE STANDARD TABLE OF zpo_header,
lt_po_item TYPE STANDARD TABLE OF zpo_item,
lv_first_idx TYPE i.
LOOP AT lt_po_header ASSIGNING FIELD-SYMBOL(<fs_hdr>).
lv_first_idx = line_index( lt_po_item[ ebeln = <fs_hdr>-ebeln ] ).
IF lv_first_idx > 0.
WRITE: / 'PO', <fs_hdr>-ebeln, 'first item index =', lv_first_idx.
ELSE.
WRITE: / 'PO', <fs_hdr>-ebeln, 'has no items'.
ENDIF.
ENDLOOP.
여기서 line_index는 0 또는 양의 정수를 반환하므로 IF lv_first_idx > 0 한 줄로 존재 여부와 인덱스를 동시에 판별할 수 있다. SY-SUBRC를 검사하지 않아도 되고, READ TABLE 한 줄이 사라지면서 의도가 명확해진다.
4. LINE_INDEX + READ TABLE INDEX 조합으로 위치 기반 수정
두 번째 예제는 실무에서 자주 마주치는 "특정 자재의 재고 라인을 찾아 수량을 보정"하는 시나리오다. 자재 코드로 위치를 찾되, 찾은 라인 자체뿐 아니라 그 직전 라인(예: 이전 차수)을 함께 갱신해야 한다고 가정한다.
TYPES: BEGIN OF ty_stock,
matnr TYPE matnr,
werks TYPE werks_d,
menge TYPE menge_d,
note TYPE string,
END OF ty_stock.
DATA: lt_stock TYPE STANDARD TABLE OF ty_stock,
lv_idx TYPE i,
lv_prev_idx TYPE i.
" 데이터 준비 생략
lv_idx = line_index( lt_stock[ matnr = 'MAT-A-0007' werks = '1010' ] ).
IF lv_idx > 1.
lv_prev_idx = lv_idx - 1.
READ TABLE lt_stock ASSIGNING FIELD-SYMBOL(<fs_curr>) INDEX lv_idx.
IF sy-subrc = 0.
<fs_curr>-menge = <fs_curr>-menge - 5.
<fs_curr>-note = |adjusted at { sy-datum } { sy-uzeit }|.
ENDIF.
READ TABLE lt_stock ASSIGNING FIELD-SYMBOL(<fs_prev>) INDEX lv_prev_idx.
IF sy-subrc = 0.
<fs_prev>-note = |prior to MAT-A-0007 adjustment|.
ENDIF.
ELSEIF lv_idx = 1.
" 첫 라인이면 이전 라인 없음 — 별도 처리
WRITE: / 'No previous line to update'.
ELSE.
WRITE: / 'Target stock line not found'.
ENDIF.
이 패턴의 가치는 두 가지다. 첫째, line_index가 부수효과 없이 인덱스를 계산하기 때문에 이후 두 번의 READ TABLE ... INDEX 호출이 동일한 기준점을 공유한다. 둘째, "다음 라인", "직전 라인" 같은 상대 위치 계산이 자연스럽게 표현된다. SY-TABIX 기반으로 같은 일을 하면 중간 READ TABLE 한 번이 시스템 필드를 덮어쓰면서 의도가 흐려진다.
5. LINE_EXISTS로 존재 확인 후 안전 접근
세 번째 예제는 SalesOrder 변경 트랜잭션의 일부로, 특정 BillingPlan 항목이 이미 존재하는 경우에만 갱신하고, 없으면 신규 추가하는 루틴이다. line_exists를 가드로 둔 뒤 테이블 표현식으로 직접 접근하면 코드가 매우 짧아진다.
TYPES: BEGIN OF ty_billing,
vbeln TYPE vbeln,
fplnr TYPE string,
amount TYPE p LENGTH 11 DECIMALS 2,
status TYPE c LENGTH 1,
END OF ty_billing.
DATA: lt_billing TYPE SORTED TABLE OF ty_billing
WITH UNIQUE KEY vbeln fplnr.
DATA(lv_target_vbeln) = CONV vbeln( '0010000123' ).
DATA(lv_target_fplnr) = `BP-Q2-2026`.
IF line_exists( lt_billing[ vbeln = lv_target_vbeln
fplnr = lv_target_fplnr ] ).
ASSIGN lt_billing[ vbeln = lv_target_vbeln
fplnr = lv_target_fplnr ] TO FIELD-SYMBOL(<fs_bp>).
<fs_bp>-amount = <fs_bp>-amount + 1500.
<fs_bp>-status = 'U'.
ELSE.
INSERT VALUE #( vbeln = lv_target_vbeln
fplnr = lv_target_fplnr
amount = 1500
status = 'N' ) INTO TABLE lt_billing.
ENDIF.
line_exists 분기 안에서는 동일한 테이블 표현식으로 접근해도 절대 예외가 발생하지 않는다. SY-SUBRC 체크 없이도 안전성이 보장된다는 점이 핵심이다. 다만 ASSIGN으로 받은 필드 심볼을 통해 수정해야 한다는 점은 잊지 말아야 한다. lt_billing[ ... ]-amount = ... 같이 표현식 좌변에 직접 대입하는 문법은 허용되지 않는 컨텍스트가 많기 때문이다.
6. FIELD-SYMBOLS와 LINE_INDEX 결합 — 참조 타입 성능 패턴
대용량 SalesOrder 라인(수십만 건)을 다룰 때 성능 차이는 두 곳에서 발생한다. 첫째, 값 복사 대신 필드 심볼/참조로 작업해 메모리 복사를 피하는 것. 둘째, 같은 테이블을 여러 번 탐색하지 않도록 인덱스를 캐싱하는 것. 다음 패턴은 두 가지를 결합한다.
TYPES: BEGIN OF ty_so_item,
vbeln TYPE vbeln,
posnr TYPE posnr,
matnr TYPE matnr,
netwr TYPE netwr_ap,
flagged TYPE abap_bool,
END OF ty_so_item.
DATA lt_so_item TYPE STANDARD TABLE OF ty_so_item
WITH NON-UNIQUE SORTED KEY by_matnr
COMPONENTS matnr.
" 데이터 적재 생략 — 200,000 라인 가정
DATA(lt_target_matnr) = VALUE string_table(
( |MAT-X-001| ) ( |MAT-X-014| ) ( |MAT-Y-093| ) ).
LOOP AT lt_target_matnr INTO DATA(lv_matnr).
DATA(lv_idx) = line_index( lt_so_item[ KEY by_matnr
COMPONENTS matnr = lv_matnr ] ).
IF lv_idx = 0.
CONTINUE.
ENDIF.
LOOP AT lt_so_item ASSIGNING FIELD-SYMBOL(<fs_item>)
FROM lv_idx
USING KEY by_matnr
WHERE matnr = lv_matnr.
<fs_item>-flagged = abap_true.
<fs_item>-netwr = <fs_item>-netwr * '0.95'.
ENDLOOP.
ENDLOOP.
여기서 line_index가 보조 키(secondary key)를 사용하도록 명시했다. 보조 키가 있는 STANDARD 테이블에서 키 지정 없이 line_index를 호출하면 선형 검색이 일어날 수 있으므로, KEY by_matnr COMPONENTS ... 구문으로 정렬 키를 명시적으로 지정하는 게 일반적으로 권장된다. 이후 LOOP ... FROM lv_idx USING KEY로 같은 키 그룹의 라인만 순회하면 불필요한 풀스캔을 피할 수 있다. 필드 심볼로 작업하므로 작업 영역 복사 비용은 0이다.
7. 주의사항 — 조건 불일치 시 cx_sy_itab_line_not_found 처리
표현식 기반 접근이 깔끔하지만, 가드 없이 사용하면 런타임 예외 CX_SY_ITAB_LINE_NOT_FOUND가 발생한다. line_exists나 line_index 자체는 예외를 던지지 않지만, 테이블 표현식을 직접 사용하는 코드(예: 대입문 우변, ASSIGN의 인자, INTO 타깃의 우변)는 일치 라인이 없으면 즉시 예외가 올라온다.
TRY.
DATA(ls_match) = lt_so_item[ vbeln = '0050001234'
posnr = '000010' ].
" 정상 흐름 ...
CATCH cx_sy_itab_line_not_found INTO DATA(lo_ex).
MESSAGE |SO item missing: { lo_ex->get_text( ) }| TYPE 'I'.
ENDTRY.
실무 코딩 가이드에서는 두 가지 스타일이 공존한다. 하나는 위처럼 TRY/CATCH로 감싸는 방식, 다른 하나는 사전에 line_exists로 검사한 뒤 분기 내부에서만 표현식을 사용하는 방식이다. 두 방식의 차이는 의도 표현에 있다. 예외 처리는 "정상 경로는 라인이 존재해야 한다"는 계약을 강조하고, line_exists 분기는 "양쪽 모두 정상 경로"라는 의미를 더 잘 전달한다.
또 하나 자주 묻는 질문은 FAQ로 정리한다.
- Q1.
line_index가 0을 반환할 때SY-SUBRC도 4가 되나요? 아니다. 두 함수 모두 시스템 필드를 건드리지 않는다.SY-SUBRC의 값은 직전 명령의 결과 그대로 유지된다. - Q2. HASHED 테이블에서도
line_index가 동작하나요? 동작하지만, HASHED 테이블은 인덱스 개념이 없으므로 반환값은 라인이 존재하면 0이 아닌 어떤 정수로 해석된다. HASHED에서는 일반적으로line_exists만 쓰고, 위치가 필요하면 SORTED 또는 STANDARD + 보조 키 구성을 검토하는 편이 안전하다. - Q3. 표현식 안에서 여러 조건을 AND로 묶고 싶으면?
lt_so_item[ vbeln = lv_v posnr = lv_p ]처럼 키-값 쌍을 공백으로 나열하면 AND로 해석된다. OR 조건은 표현식 안에서 직접 표현할 수 없으므로FILTER또는LOOP ... WHERE를 함께 사용한다.
8. SY-TABIX vs LINE_INDEX 비교 정리 및 다음으로 살펴볼 주제
| 관점 | SY-TABIX 패턴 | LINE_INDEX 패턴 |
| 부수효과 | 거의 모든 테이블 명령이 갱신 | 호출해도 시스템 필드 불변 |
| 존재 확인 | SY-SUBRC 별도 검사 | 반환값 0 비교 또는 line_exists |
| 가독성 | 두세 줄에 의도 분산 | 한 표현식으로 의도 응축 |
| 예외 위험 | 잘못된 TABIX 재사용 | 표현식 직접 사용 시 CX_SY_ITAB_LINE_NOT_FOUND |
| 키 활용 | 암시적 | USING KEY / COMPONENTS로 명시 가능 |
요약하면, 새 코드에서는 가능한 한 표현식 기반 접근으로 옮기되, 인덱스가 필요한 곳에서만 line_index를 사용하고, 단순 존재 확인은 line_exists로 표현하는 것이 일반적으로 권장된다. 이 글의 패턴을 익혔다면 이어서 살펴보면 좋을 주제는 FILTER 표현식으로 부분 집합 추출하기, REDUCE/FOR 루프로 집계 표현식 작성하기, 그리고 ABAP 7.55 이후의 향상된 보조 키 자동 선택 동작이다. RAP(ABAP RESTful Application Programming) 환경에서 비즈니스 객체 핸들러 메서드를 짤 때도 이 패턴이 그대로 적용된다.
더 깊이 파고들 만한 자료
- SAP Help Portal — ABAP Keyword Documentation: line_exists / line_index (help.sap.com)
- SAP Help Portal — Table Expressions와 예외 처리 (help.sap.com)
- SAP Help Portal — Secondary Keys for Internal Tables (help.sap.com)
- SAP Community 블로그 — Modern ABAP: Replacing READ TABLE with Table Expressions
- SAP Press — "ABAP Development for SAP S/4HANA" 내부 테이블 챕터
댓글 0
아직 댓글이 없습니다.