개요 및 이 글에서 얻어갈 것
SAP S/4HANA 환경에서 배송(Outbound Delivery) 데이터를 조회할 때, 여전히 관행처럼 LIPS(배송 아이템)와 LIKP(배송 헤더) 물리 테이블을 직접 SELECT하는 코드가 많습니다. 이 접근은 몇 년 전까지는 사실상 표준이었지만, S/4HANA 이후로는 I_DeliveryItem과 같은 표준 Basic CDS View를 사용하는 방식이 권장됩니다. 이 글은 LIPS 직접 조회의 구조적 문제부터 I_DeliveryItem을 활용한 실무 조회 패턴, 그리고 Association을 이용한 배송 헤더-아이템 연결까지 단계별로 다룹니다.
- LIPS/LIKP 직접 조회의 문제점을 구조적으로 파악할 수 있는지 확인
I_DeliveryItem의 필드 매핑과 Association 구조 이해- 배송 헤더-아이템 조인을 CDS Association으로 대체할 수 있는지 확인
- 특정 판매 오더나 GoodsMovementStatus 기반의 실전 필터링 구현
- 대량 조회 시 성능/권한/클라이언트 처리 관점의 주의점 학습
미리 알아두면 좋은 배경
이 글은 ABAP OpenSQL과 CDS View의 기본 개념(SELECT ... FROM, @Consumption.filter 정도의 어노테이션)을 이해하고, SD(판매 및 유통) 모듈의 배송 문서 구조(헤더-아이템, 판매 오더 참조)에 익숙한 개발자를 대상으로 합니다. Fiori Elements나 RAP를 반드시 알 필요는 없지만, Basic View와 Composite View의 계층 개념을 알고 있다면 후반부 이해가 훨씬 빠릅니다.
실행 환경과 사전 준비
이 글의 예제는 다음 환경을 기준으로 작성했습니다.
- SAP S/4HANA 2022 또는 2023 (On-Premise) / SAP S/4HANA Cloud Public Edition 최신 릴리스
- ABAP Development Tools(ADT) for Eclipse 3.34 이상
- 테스트 클라이언트에서 SD 배송 문서(VBTYP = 'J' 등)가 최소 수십 건 존재
- 사용자에게 CDS View
I_DeliveryItem에 대한 SELECT 권한(S_RS_COMP, DDIC 조회 권한) 부여
Cloud 에디션의 경우 물리 테이블 LIPS/LIKP 자체에 대한 SELECT는 릴리스 계약에 따라 제한될 수 있으므로, 이 글에서 다루는 CDS 접근법을 처음부터 채택하는 것이 안전합니다. On-Premise 환경이더라도 CDS View 활용은 향후 Cloud 마이그레이션 시 코드 재작성 부담을 크게 줄여 줍니다.
핵심 개념 — LIPS 직접 조회 대신 I_DeliveryItem을 쓰는 이유
LIPS는 배송 아이템의 물리 저장 테이블이고, LIKP는 배송 헤더의 저장 테이블입니다. 예전 ECC 환경에서 배송 데이터를 다룰 때는 SELECT ... FROM lips INNER JOIN likp ON lips~vbeln = likp~vbeln 형태로 조인해서 원하는 필드를 뽑는 것이 일반적이었습니다. 여기에 자재 마스터(MARA), 판매 오더 참조(VBAK/VBAP), 상태 테이블(VBUK/VBUP) 등이 얽히면 조인이 5~6단으로 늘어나고, 클라이언트 필드(MANDT) 처리도 개발자가 신경 써야 했습니다.
이 접근의 실무적 문제는 세 가지로 요약됩니다. 첫째, 원시 테이블은 릴리스마다 필드가 추가·비활성화될 수 있어 코드 안정성이 낮습니다. 예를 들어 S/4HANA에서 VBUK/VBUP는 이미 완전히 사라졌고, 그 상태 정보는 LIPS/LIKP의 필드로 병합됐습니다. 둘째, Cloud 에디션에서는 물리 테이블 자체가 개발자에게 노출되지 않는 경우가 많습니다. 셋째, 물리 테이블 SELECT는 필드 레벨 권한 체크(S/4의 ACDOCA류 인증 개념)를 우회해 버리므로, 감사 관점에서도 취약합니다.
I_DeliveryItem은 이 문제를 해결하기 위한 SAP 표준 "Interface View" 계층의 Basic View입니다. 흔히 배송 아이템을 라면에 비유하자면, LIPS는 원료 상태의 생면과 스프이고, I_DeliveryItem은 그것을 표준 레시피로 조리해 놓은 완성품에 가깝습니다. 필드 이름이 사람이 읽을 수 있는 시맨틱 이름(DeliveryDocument, DeliveryDocumentItem, Material, ActualDeliveryQuantity 등)으로 바뀌어 있고, 클라이언트 필드는 프레임워크가 자동 처리하며, 헤더/판매오더/자재 마스터로의 Association(_DeliveryDocument, _SalesOrder, _Material)이 이미 정의되어 있습니다.
Association은 SQL의 명시적 JOIN과 달리, 실제로 참조된 필드가 있을 때만 런타임에 조인이 만들어집니다. 즉 SELECT DeliveryDocument, DeliveryDocumentItem FROM I_DeliveryItem만 하면 헤더 테이블은 조인되지 않고, _DeliveryDocument.ShippingPoint를 참조하는 순간에만 헤더가 조인됩니다. 이 "지연 조인" 특성 때문에 CDS 기반 조회는 잘 짜면 물리 테이블 직접 조인보다 오히려 빠릅니다.
실전 예제 1단계 — 기본 조회 대체
가장 먼저, 특정 배송 문서 번호에 속한 아이템의 자재, 수량, 출하 플랜트를 가져오는 예제를 봅니다. 기존 방식과 CDS 방식을 나란히 두면 차이가 뚜렷합니다.
" 예전 방식: LIPS 직접 조회 (참고용, 신규 개발에는 지양)
SELECT vbeln, posnr, matnr, lfimg, werks
FROM lips
INTO TABLE @DATA(lt_lips_old)
WHERE vbeln = @iv_delivery_no.
" 권장 방식: I_DeliveryItem CDS View 사용
SELECT DeliveryDocument,
DeliveryDocumentItem,
Material,
ActualDeliveryQuantity,
DeliveryQuantityUnit,
Plant
FROM I_DeliveryItem
INTO TABLE @DATA(lt_delivery_items)
WHERE DeliveryDocument = @iv_delivery_no.
필드 이름만 봐도 어떤 값이 들어오는지 명확합니다. MATNR 대신 Material, LFIMG(배송 수량) 대신 ActualDeliveryQuantity, WERKS 대신 Plant가 사용됩니다. 단위 필드도 MEINS가 아니라 DeliveryQuantityUnit으로 시맨틱이 부여되어 있어, 후속 처리 코드에서 주석 없이도 읽힙니다.
실전 예제 2단계 — 헤더 정보와 판매 오더 참조까지 한 번에
실무에서는 아이템만 가져오는 경우가 드뭅니다. 배송 헤더의 출하 지점(ShippingPoint), 실제 배송일(ActualGoodsMovementDate), 그리고 각 아이템이 참조하는 판매 오더 번호(ReferenceSDDocument)까지 함께 필요할 때가 많습니다. 기존 방식이라면 LIPS × LIKP × VBAP 세 테이블을 조인해야 하지만, Association을 쓰면 다음처럼 정리됩니다.
TYPES: BEGIN OF ty_delivery_row,
delivery_no TYPE i_deliveryitem-deliverydocument,
item_no TYPE i_deliveryitem-deliverydocumentitem,
material TYPE i_deliveryitem-material,
qty TYPE i_deliveryitem-actualdeliveryquantity,
plant TYPE i_deliveryitem-plant,
shipping_point TYPE i_deliverydocument-shippingpoint,
actual_gm_date TYPE i_deliverydocument-actualgoodsmovementdate,
ref_sales_order TYPE i_deliveryitem-referencesddocument,
ref_item TYPE i_deliveryitem-referencesddocumentitem,
END OF ty_delivery_row.
DATA(lt_report) = VALUE STANDARD TABLE OF ty_delivery_row( ).
TRY.
SELECT di~DeliveryDocument AS delivery_no,
di~DeliveryDocumentItem AS item_no,
di~Material AS material,
di~ActualDeliveryQuantity AS qty,
di~Plant AS plant,
di~\_DeliveryDocument-ShippingPoint AS shipping_point,
di~\_DeliveryDocument-ActualGoodsMovementDate AS actual_gm_date,
di~ReferenceSDDocument AS ref_sales_order,
di~ReferenceSDDocumentItem AS ref_item
FROM I_DeliveryItem AS di
INTO CORRESPONDING FIELDS OF TABLE @lt_report
WHERE di~ReferenceSDDocument = @iv_sales_order_no.
IF sy-subrc <> 0.
MESSAGE |No delivery items for sales order { iv_sales_order_no }| TYPE 'S'.
RETURN.
ENDIF.
" 로깅: 애플리케이션 로그(SLG1)로 요약 남기기
cl_bali_log_db=>get_instance( )->save(
i_log = cl_bali_log_factory=>create_log(
VALUE #( object = 'ZDELIV'
subobject = 'READ'
external_id = |SO { iv_sales_order_no }| ) ) ).
CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
" 인프라 예외를 도메인 예외로 감싸서 상위로 전달
RAISE EXCEPTION TYPE zcx_delivery_read
EXPORTING previous = lx_sql.
ENDTRY.
여기서 핵심은 di~\_DeliveryDocument-ShippingPoint 형태의 path expression입니다. _DeliveryDocument는 I_DeliveryItem에 이미 정의된 Association으로, 대상은 I_DeliveryDocument입니다. 실제 SQL로 번역될 때만 LIKP에 해당하는 물리 조인이 발생하며, 사용하지 않으면 조인 자체가 생성되지 않습니다. 또한 참조 판매 오더는 ReferenceSDDocument(=예전 VGBEL)와 ReferenceSDDocumentItem(=VGPOS)으로 이름이 통일돼 있어, 판매 오더 아이템으로 이어질 _ReferenceSDDocumentItem Association을 그대로 활용할 수 있습니다.
실전 예제 3단계 — 상태 기반 필터링과 페이징을 곁들인 프로덕션 패턴
실전 배치나 API에서는 "출고 이동(Goods Movement)이 아직 처리되지 않은 배송 아이템만" 또는 "특정 출하 지점에서 오늘 이동 예정인 아이템"과 같은 상태 조건이 붙습니다. I_DeliveryItem은 상태 필드를 헤더/아이템 관점 모두 노출합니다. 대표적으로 GoodsMovementStatus(A=미처리, B=부분처리, C=완료)와 OverallDeliveryStatus가 있습니다.
CLASS zcl_delivery_reader DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_open_item,
delivery_no TYPE i_deliveryitem-deliverydocument,
item_no TYPE i_deliveryitem-deliverydocumentitem,
material TYPE i_deliveryitem-material,
open_qty TYPE i_deliveryitem-actualdeliveryquantity,
plant TYPE i_deliveryitem-plant,
shipping_point TYPE i_deliverydocument-shippingpoint,
gm_status TYPE i_deliveryitem-goodsmovementstatus,
END OF ty_open_item,
tt_open_item TYPE STANDARD TABLE OF ty_open_item WITH EMPTY KEY.
METHODS read_open_items
IMPORTING iv_shipping_point TYPE i_deliverydocument-shippingpoint
iv_page_size TYPE i DEFAULT 500
iv_page_offset TYPE i DEFAULT 0
RETURNING VALUE(rt_items) TYPE tt_open_item
RAISING zcx_delivery_read.
ENDCLASS.
CLASS zcl_delivery_reader IMPLEMENTATION.
METHOD read_open_items.
TRY.
SELECT di~DeliveryDocument AS delivery_no,
di~DeliveryDocumentItem AS item_no,
di~Material AS material,
di~ActualDeliveryQuantity AS open_qty,
di~Plant AS plant,
di~\_DeliveryDocument-ShippingPoint AS shipping_point,
di~GoodsMovementStatus AS gm_status
FROM I_DeliveryItem AS di
WHERE di~\_DeliveryDocument-ShippingPoint = @iv_shipping_point
AND di~GoodsMovementStatus IN ( 'A', 'B' )
AND di~ActualDeliveryQuantity > 0
ORDER BY di~DeliveryDocument, di~DeliveryDocumentItem
INTO CORRESPONDING FIELDS OF TABLE @rt_items
OFFSET @iv_page_offset
UP TO @iv_page_size ROWS.
CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
RAISE EXCEPTION TYPE zcx_delivery_read
EXPORTING previous = lx_sql.
ENDTRY.
ENDMETHOD.
ENDCLASS.
이 구현이 프로덕션 관점에서 좋은 이유는 다음과 같습니다. 첫째, OFFSET ... UP TO ... ROWS로 페이징을 강제해 한 번의 호출이 백만 건을 긁어오는 사고를 막습니다. 둘째, WHERE 조건에서 상태와 수량을 함께 걸어 DB 레벨에서 필터링해, 애플리케이션 서버로 전송되는 데이터가 최소화됩니다. 셋째, 예외를 인프라(cx_sy_open_sql_db) → 도메인(zcx_delivery_read)으로 감싸 호출자가 CDS 세부 구현에 결합되지 않도록 합니다. 넷째, CDS View에는 이미 필드 레벨 권한과 릴리스 계약(Released API)이 걸려 있어, 물리 테이블 직접 조회에 비해 감사 대응이 훨씬 수월합니다.
자주 부딪히는 실수와 문제 해결
첫 번째는 클라이언트 필드 처리 실수입니다. LIPS 시절에는 WHERE mandt = sy-mandt를 빠뜨리면 잘못된 데이터가 섞였는데, CDS View는 프레임워크가 자동으로 클라이언트를 처리합니다. 대신 CLIENT SPECIFIED 없이 다른 클라이언트를 강제로 읽으려는 코드는 실행되지 않으므로, 크로스-클라이언트 유틸리티를 이식할 때 주의해야 합니다.
두 번째는 필드 이름 오해입니다. DeliveryQuantity와 ActualDeliveryQuantity는 다른 개념입니다. 전자는 계획 수량 관점, 후자는 실제 배송 수량 관점에 가깝습니다. 어떤 값을 KPI로 잡을지 결정하기 전에 필드 어노테이션과 도메인 정의를 반드시 확인해야 합니다.
세 번째는 Association 오남용입니다. _DeliveryDocument를 SELECT 절과 WHERE 절 모두에서 반복 참조하면 옵티마이저가 헤더 조인을 한 번만 생성해 재사용하지만, 조건에 OR로 얽어 놓으면 조인 두 번을 생성하는 계획으로 뒤집힐 수 있습니다. 대용량 조회에서는 반드시 SET RUN TIME ANALYZER나 SQL Monitor(ST04)로 실제 실행 계획을 확인해야 합니다.
Q1. LIPS를 아예 조회하면 안 되나요? 조회는 가능합니다. 다만 신규 개발과 리팩토링 대상 코드에서는 CDS 사용이 권장되며, Cloud 이관을 염두에 둔 코드라면 처음부터 CDS만 쓰는 편이 안전합니다.
Q2. I_DeliveryItem의 필드가 부족한데요? Basic View는 저수준이라 UI 라벨이 없습니다. 계산 필드나 라벨이 필요하면 C_DeliveryItem 계열의 Consumption View나 자체 커스텀 View를 I_DeliveryItem 위에 얹으세요.
Q3. 성능이 예전 LIPS 조인보다 느려요. 대개 SELECT * 관행이나 불필요한 Association 참조가 원인입니다. 필요한 컬럼만 명시하고, WHERE 조건이 인덱스로 유도되는지 ST05로 확인하세요.
이어서 살펴보면 좋은 주제
이 글에서 배운 I_DeliveryItem 조회 패턴은 다음 단계로 자연스럽게 이어집니다. 먼저 I_DeliveryDocument와의 Composite View 조합을 통해 헤더-아이템을 하나의 논리 모델로 노출하는 방법을 익힐 수 있습니다. 이어서 RAP(RESTful ABAP Programming Model) 기반으로 배송 아이템을 조회/변경하는 Behavior Definition, 그리고 Fiori Elements List Report에 바인딩해서 UI까지 만드는 흐름을 학습하면 실무 프로젝트에 바로 투입 가능한 스택이 완성됩니다. 병행해 SD 모듈의 상태 필드(OverallDeliveryStatus, OverallGoodsMovementStatus) 도메인 값과 BAPI/BOPF 이벤트 트리거 시점을 정리해 두면 상태 기반 배치 설계에 큰 도움이 됩니다.
더 깊이 파고들 수 있는 링크
댓글 0
아직 댓글이 없습니다.