ABAP

EKPO vs I_PurchaseOrderItem — CDS 뷰 선택 이유 #shorts #SAP #ABAP

이 예제로 무엇을 얻어갈 수 있는가

SAP S/4HANA 환경에서 구매 오더 아이템 데이터를 다룰 때 개발자들이 가장 먼저 마주치는 선택은 "EKPO를 직접 SELECT할 것인가, 아니면 I_PurchaseOrderItem CDS 뷰를 쓸 것인가"입니다. 이 글에서는 S/4HANA Virtual Data Model(VDM) 계층에서 Basic View로 정의된 I_PurchaseOrderItem의 구조·용도·실전 활용법을 다룹니다.

  • I_PurchaseOrderItem의 내부 구조와 EKPO 대비 이점 이해
  • ABAP RESTful Application Programming(RAP) 및 리포트에서 CDS 뷰 조회 패턴 숙지
  • OData 서비스 노출과 Fiori Elements 연동 흐름 파악
  • 필드 선택·필터 푸시다운을 이용한 성능 최적화 기법 적용
  • EKPO 직접 조회 코드를 CDS 기반으로 마이그레이션할 때의 체크포인트 확보

미리 알고 있어야 하는 것

ABAP Open SQL의 SELECT 문법과 조인 개념, EKKO/EKPO 등 구매 관련 표준 테이블의 대략적인 역할, 그리고 CDS(Core Data Services) DDL의 기본 문법(define view, association, @ObjectModel 어노테이션 등)을 알고 있어야 합니다. Eclipse ADT(ABAP Development Tools) 설치 상태와 S/4HANA 시스템 접근 권한도 필요합니다.

실습 환경 및 준비물

이 글의 코드는 다음 환경을 기준으로 검증되었습니다.

  • SAP S/4HANA 2022 On-Premise 또는 S/4HANA Cloud Public Edition 2023 이상
  • ABAP Platform 릴리스 7.57 이상 (Release-Independent CDS 지원)
  • Eclipse 2023-03 + ABAP Development Tools 3.36 이상
  • 테스트 데이터: 트랜잭션 ME21N으로 생성한 구매 오더 몇 건, 또는 표준 데모 데이터셋
  • 권한: S_DEVELOP, S_RS_RDEEV(CDS 뷰 조회 시)

S/4HANA 1909 이전 릴리스에서는 I_PurchaseOrderItem이 존재하지 않거나 필드 셋이 다를 수 있습니다. 실제 사용 전에 SE11 또는 ADT의 Data Preview로 필드 목록을 확인하는 것이 일반적으로 권장됩니다.

핵심 개념: VDM 계층에서 I_PurchaseOrderItem이 놓인 위치

SAP S/4HANA는 데이터 접근 표준을 트랜잭션 테이블 직접 조회에서 VDM(Virtual Data Model) 기반으로 옮겨왔습니다. VDM은 세 개의 계층으로 이루어집니다.

  • Basic View (I_*): 트랜잭션 테이블에 1:1로 가까운 뷰. 필드명이 시맨틱하게 정리되어 있음
  • Composite View (C_*): 여러 Basic View를 조합해 분석·리포팅에 적합한 형태로 재구성
  • Consumption View (C_*, UI 서비스용): Fiori/OData 소비 계층

I_PurchaseOrderItem은 이 중 Basic Interface View에 해당하며, 물리적으로는 EKPO 테이블을 소스로 하되 다음의 부가 기능을 얹어 제공합니다.

  • 시맨틱 필드명: EBELN 대신 PurchaseOrder, MATNR 대신 Material, MENGE 대신 OrderQuantity
  • 연관(Association): _PurchaseOrder(→ I_PurchaseOrder), _Material(→ I_Material), _Plant 등으로 조인 없이 데이터 탐색 가능
  • 어노테이션 기반 메타데이터: 통화·단위 필드 자동 매핑(@Semantics.amount.currencyCode), UI 라벨, OData 노출 힌트
  • 버전 안정성: EKPO에 신규 컬럼이 추가되어도 CDS 계약은 하위 호환을 유지하는 방향으로 관리됨

비유하자면 EKPO는 원자재 창고이고, I_PurchaseOrderItem은 창고를 표준 포장·라벨링해 배송 가능한 상태로 꺼내주는 물류 센터입니다. 여러분의 애플리케이션은 창고 문을 직접 열 필요 없이 표준 라벨을 신뢰하고 코드를 짤 수 있습니다.

EKPO 직접 조회와의 비교

동일한 요구사항(특정 플랜트의 미완결 구매 아이템 조회)을 두 방식으로 구현했을 때 차이를 보겠습니다.

" 전통적 EKPO 직접 조회
SELECT ekpo~ebeln, ekpo~ebelp, ekpo~matnr, ekpo~werks, ekpo~menge, ekpo~netpr, ekpo~waers
  FROM ekpo
  INNER JOIN ekko ON ekko~ebeln = ekpo~ebeln
  INTO TABLE @DATA(lt_legacy)
  WHERE ekpo~werks = @lv_plant
    AND ekpo~loekz = @space
    AND ekko~bstyp = 'F'.

위 코드는 삭제 플래그(LOEKZ)와 문서 타입(BSTYP)이라는 도메인 지식을 개발자가 직접 알고 있어야 하며, 다국어 화폐/수량 처리도 별도 로직이 필요합니다. CDS 뷰 기반으로 바꾸면 다음과 같이 단순화됩니다.

SELECT PurchaseOrder, PurchaseOrderItem, Material, Plant,
       OrderQuantity, PurchaseOrderQuantityUnit,
       NetPriceAmount, DocumentCurrency
  FROM I_PurchaseOrderItem
  WHERE Plant = @lv_plant
    AND PurchaseOrderItemIsDeleted = @abap_false
  INTO TABLE @DATA(lt_items).

1단계 예제: 기본 조회와 필드 이해

먼저 특정 구매 오더 번호로 아이템 라인을 조회하고, 주요 필드가 어떻게 매핑되는지 확인하는 예제입니다.

REPORT z_po_item_basic_read.

PARAMETERS: p_ebeln TYPE ebeln OBLIGATORY DEFAULT '4500000123'.

DATA: lt_items TYPE TABLE OF I_PurchaseOrderItem.

SELECT PurchaseOrder,
       PurchaseOrderItem,
       Material,
       PurchaseOrderItemText,
       Plant,
       StorageLocation,
       OrderQuantity,
       PurchaseOrderQuantityUnit,
       NetPriceAmount,
       NetPriceQuantity,
       DocumentCurrency
  FROM I_PurchaseOrderItem
  WHERE PurchaseOrder = @p_ebeln
  INTO CORRESPONDING FIELDS OF TABLE @lt_items.

LOOP AT lt_items INTO DATA(ls_item).
  WRITE: / ls_item-PurchaseOrder,
           ls_item-PurchaseOrderItem,
           ls_item-Material,
           ls_item-OrderQuantity UNIT ls_item-PurchaseOrderQuantityUnit,
           ls_item-NetPriceAmount CURRENCY ls_item-DocumentCurrency.
ENDLOOP.

여기서 주목할 점은 OrderQuantityPurchaseOrderQuantityUnit이 어노테이션으로 짝지어져 있어 WRITE 문의 UNIT 옵션이 자연스럽게 동작한다는 것입니다. EKPO에서 MENGEMEINS를 따로 캐리하던 것이 시맨틱하게 묶인 셈입니다.

2단계 예제: 실무 리포트 - 연관 뷰와 예외 처리

실무에서는 아이템만이 아니라 헤더 정보(공급업체, 문서 일자), 자재 마스터의 자재 그룹 등을 함께 봐야 합니다. Association을 활용하면 조인문 없이 필요한 데이터를 끌어옵니다.

CLASS zcl_po_item_report DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_report_line,
             purchase_order   TYPE ebeln,
             po_item          TYPE ebelp,
             supplier         TYPE lifnr,
             material         TYPE matnr,
             material_group   TYPE matkl,
             plant            TYPE werks_d,
             quantity         TYPE ekpo-menge,
             unit             TYPE meins,
             net_amount       TYPE ekpo-netwr,
             currency         TYPE waers,
           END OF ty_report_line.

    METHODS get_open_items
      IMPORTING iv_plant       TYPE werks_d
                iv_date_from   TYPE d
      RETURNING VALUE(rt_data) TYPE STANDARD TABLE OF ty_report_line.
ENDCLASS.

CLASS zcl_po_item_report IMPLEMENTATION.
  METHOD get_open_items.

    TRY.
        SELECT poi~PurchaseOrder,
               poi~PurchaseOrderItem,
               poi~_PurchaseOrder-Supplier   AS supplier,
               poi~Material,
               poi~_Material-MaterialGroup   AS material_group,
               poi~Plant,
               poi~OrderQuantity             AS quantity,
               poi~PurchaseOrderQuantityUnit AS unit,
               poi~NetPriceAmount            AS net_amount,
               poi~DocumentCurrency          AS currency
          FROM I_PurchaseOrderItem AS poi
          WHERE poi~Plant = @iv_plant
            AND poi~_PurchaseOrder-PurchaseOrderDate >= @iv_date_from
            AND poi~PurchaseOrderItemIsDeleted = @abap_false
          INTO CORRESPONDING FIELDS OF TABLE @rt_data.

        IF sy-subrc <> 0.
          MESSAGE i398(00) WITH 'No purchase items found' iv_plant.
        ENDIF.

      CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
        MESSAGE lx_sql->get_text( ) TYPE 'E'.
    ENDTRY.

  ENDMETHOD.
ENDCLASS.

Association 경로(_PurchaseOrder-Supplier)는 뒤에서 자동으로 LEFT OUTER JOIN으로 전개됩니다. 헤더 조인을 손으로 쓰지 않아도 되어 코드가 요구사항에 집중됩니다.

3단계 예제: 프로덕션 - RAP 서비스와 OData 노출

이번에는 Fiori Elements 앱이 소비할 수 있는 읽기 전용 OData 서비스를 만들고, 캐시·권한을 얹은 프로덕션 수준의 접근을 보겠습니다. Composition 뷰를 하나 만들고, 이를 Service Definition에 등록합니다.

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Purchase Order Item - Reporting Consumption'
@Metadata.allowExtensions: true
@Search.searchable: true
define view entity ZC_PurchaseOrderItemRpt
  as select from I_PurchaseOrderItem as poi
  association [0..1] to I_Supplier   as _Supplier on _Supplier.Supplier = poi._PurchaseOrder.Supplier
{
  key poi.PurchaseOrder,
  key poi.PurchaseOrderItem,
      @Search.defaultSearchElement: true
      poi.Material,
      poi.Plant,
      poi.OrderQuantity,
      poi.PurchaseOrderQuantityUnit,
      @Semantics.amount.currencyCode: 'DocumentCurrency'
      poi.NetPriceAmount,
      poi.DocumentCurrency,
      poi._PurchaseOrder.PurchaseOrderDate,
      poi._PurchaseOrder.Supplier,
      _Supplier.SupplierName,
      _Supplier
}
@EndUserText.label: 'Service Definition for PO Item Reporting'
define service ZUI_PurchaseOrderItemRpt {
  expose ZC_PurchaseOrderItemRpt as PurchaseOrderItem;
  expose I_Supplier               as Supplier;
}

Service Binding을 활성화한 뒤 Preview 버튼을 누르면 Fiori Elements List Report가 즉시 렌더링됩니다. 여기까지가 CDS + RAP의 표준 접근입니다.

성능 관점에서 중요한 두 가지 원칙이 있습니다.

  • 필요한 필드만 SELECT: SELECT *는 CDS 뷰의 모든 파생 계산과 currency conversion을 트리거해 응답을 무겁게 만듭니다.
  • 필터 푸시다운: HANA로 필터가 내려가도록 WHERE Plant = @lv_plant처럼 인덱스가 있는 필드부터 명시하고, 계산 필드에 필터를 걸지 않습니다.
" 프로덕션 조회: 페이징 + 필드 최소화
SELECT PurchaseOrder, PurchaseOrderItem, Material, NetPriceAmount, DocumentCurrency
  FROM I_PurchaseOrderItem
  WHERE Plant       = @lv_plant
    AND CompanyCode = @lv_bukrs
  ORDER BY PurchaseOrder, PurchaseOrderItem
  INTO TABLE @DATA(lt_page)
  UP TO 200 ROWS
  OFFSET @lv_offset.

자주 마주치는 문제와 해결 방향

Q1. "필드가 없다"는 컴파일 에러가 나요.
EKPO의 기술적 컬럼(예: UPTYP, WEBAZ)이 Basic View에 전부 노출되지 않는 경우가 있습니다. ADT의 F2(Element Info)로 CDS 필드 목록을 먼저 확인하고, 누락되었으면 Extension View(extend view entity)로 프로젝트 전용 필드를 붙이는 방식을 검토합니다.

Q2. Fiori 앱에서 통화 값이 이상하게 표시됩니다.
@Semantics.amount.currencyCode 어노테이션과 짝지어진 통화 필드(DocumentCurrency)를 함께 SELECT하지 않으면 프론트에서 스케일링이 잘못됩니다. Amount 필드는 반드시 통화 필드와 함께 노출하는 것이 일반적으로 권장됩니다.

Q3. EKPO 대비 쿼리가 오히려 느립니다.
연관(Association)을 SELECT 절에서 남용하면 뒤에서 다중 조인이 전개되어 옵티마이저 부담이 커집니다. 정말 필요한 필드만 참조하고, 대량 배치에서는 CDS 뷰 대신 I_PurchaseOrderItem의 소스 테이블에 대한 CDS Table Function이나 AMDP를 검토합니다.

Q4. 마이그레이션 시 삭제 아이템 카운트가 달라졌습니다.
PurchaseOrderItemIsDeleted 필터를 걸었는지 확인합니다. EKPO에서는 LOEKZ <> 'L'로 처리하던 것이 CDS에서는 boolean 형태로 노출됩니다. 카운트 검증 스크립트를 병렬 실행해 결과를 비교하는 회귀 테스트를 두는 것이 안전합니다.

실무 마이그레이션 체크리스트

EKPO 직접 조회를 I_PurchaseOrderItem으로 교체할 때 놓치기 쉬운 항목을 정리합니다.

  • LOEKZ = spacePurchaseOrderItemIsDeleted = abap_false 로 변환
  • BSTYP(문서 타입) 필터 → _PurchaseOrder-PurchaseOrderType Association 경로 사용
  • 다중 JOIN 코드 → Association 경로 단일 참조로 압축
  • 통화/수량 필드 쌍(MENGE/MEINS, NETWR/WAERS) → 어노테이션 쌍으로 SELECT
  • ABAP Unit 테스트: cl_cds_test_environment로 CDS 뷰에 더미 데이터 주입해 단위 검증
  • 병렬 실행 비교: 기존 쿼리 결과 vs CDS 뷰 결과 행 수·금액 합계 일치 여부 확인

댓글 0

아직 댓글이 없습니다.