개요 및 이 글에서 다루는 핵심 포인트
SAP S/4HANA 환경에서 구매 오더 헤더 데이터를 다룰 때, 과거 ABAP 개발자들은 EKKO 테이블을 직접 SELECT 하는 방식에 익숙했습니다. 그러나 S/4HANA 도입 이후 SAP는 VDM(Virtual Data Model) 기반의 CDS View를 표준 데이터 액세스 계층으로 권장하고 있으며, 그 대표적인 사례가 바로 I_PurchaseOrder입니다. 이 글은 EKKO 테이블의 한계를 보완하고 표준화된 데이터를 제공하는 I_PurchaseOrder의 구조, 조인 관계, 실전 활용법을 단계별 예제로 풀어갑니다.
- I_PurchaseOrder가 EKKO와 어떤 관계를 가지는지 이해한다
- EKKO 외에 어떤 테이블(LFA1, T001, T024 등)이 조인되어 있는지 파악한다
- Released API 관점에서 I_PurchaseOrder를 안전하게 소비하는 방법을 익힌다
- Consumption View와 Association 패턴을 실전 코드로 구현한다
- ABAP 프로그램에서 SELECT 문으로 데이터를 추출하는 실무 패턴을 습득한다
이 글을 읽기 전에 알아두면 좋은 것들
이 글은 ABAP 개발 경험이 있는 중급 개발자를 대상으로 합니다. EKKO/EKPO 테이블 구조에 대한 기본 이해, ABAP Open SQL 문법, CDS View의 기본 개념(DEFINE VIEW, ASSOCIATION TO, Annotation), 그리고 SAP S/4HANA의 VDM 계층(Basic/Composite/Consumption View) 구분을 사전에 학습해두면 따라가기 수월합니다. ADT(ABAP Development Tools for Eclipse) 사용 경험도 권장합니다.
실습 환경 및 사전 준비 사항
이 글에서 다루는 코드와 개념은 다음 환경을 기준으로 작성되었습니다.
- 시스템: SAP S/4HANA 2022 또는 그 이상 (On-Premise 또는 Private Cloud Edition)
- ABAP 릴리스: ABAP Platform 7.57 이상
- 개발 도구: Eclipse 2023-03 이상 + ABAP Development Tools (ADT) 3.34+
- 권한: S_DEVELOP, MM_MASTER 권한 그룹, 그리고 EKKO/EKPO 테이블 조회 권한
- 샘플 데이터: 표준 MM 구매 트랜잭션 데이터 (ME21N으로 생성 가능)
SAP S/4HANA Cloud Public Edition에서도 I_PurchaseOrder는 Released 상태로 제공되지만, 일반적으로 Custom CDS View나 RAP Business Object를 통해 간접적으로 소비하는 패턴이 권장됩니다. On-Premise 환경에서는 SE11이나 ADT의 View Editor를 통해 소스를 직접 확인할 수 있습니다.
I_PurchaseOrder의 본질과 EKKO와의 관계
I_PurchaseOrder는 SAP VDM 계층에서 Basic Interface View로 분류되며, 이름 앞에 붙은 'I_' 접두어가 이를 의미합니다. 이 뷰는 EKKO(구매 오더 헤더) 테이블을 핵심 기반으로 삼되, 단순히 EKKO를 1:1로 노출하는 것이 아니라 다음과 같은 부가 가치를 제공합니다.
- 필드명 표준화: EKKO의 EBELN → PurchaseOrder, BUKRS → CompanyCode 처럼 의미가 명확한 영문 필드명으로 매핑
- 관련 마스터 데이터 조인: 공급업체(LFA1), 회사코드(T001), 구매 조직(T024E), 구매 그룹(T024) 등의 텍스트 정보가 Association을 통해 즉시 접근 가능
- Released API 보장: SAP가 호환성을 일반적으로 유지하는 안정 계약이므로 업그레이드 시 영향이 최소화됨
- Analytical/Transactional 어노테이션: Fiori, OData, CDP 등 다양한 소비 채널을 위한 메타데이터가 사전 구성
비유하자면 EKKO가 '날 것의 재료가 담긴 창고'라면, I_PurchaseOrder는 '재료 + 양념 + 라벨이 표준 규격으로 포장된 키트'입니다. 개발자는 더 이상 EKKO에서 통화 단위, 공급업체 이름, 조직 텍스트를 별도 JOIN으로 가져올 필요 없이 Association 한 번으로 접근할 수 있습니다.
아래 도식은 I_PurchaseOrder를 중심으로 한 데이터 흐름을 단순화한 것입니다.
[EKKO] [LFA1 - 공급업체]
구매오더 헤더 ────────┐ ┌──── Supplier Name/Address
│ │
▼ ▼
┌──────────────────────┐
│ I_PurchaseOrder │ <── I_Supplier (Assoc)
│ (Basic VDM View) │ <── I_CompanyCode (Assoc)
└──────────────────────┘ <── I_PurchaseOrderItem (Assoc)
▲ ▲
│ │
[T001] │ │ [T024/T024E]
회사코드 ─────────────┘ └─── 구매조직/구매그룹
EKKO에 약 150개 이상의 필드가 존재하지만, I_PurchaseOrder는 그중 비즈니스적으로 유의미한 약 80~90개의 필드만 노출합니다. 사용 빈도가 낮거나 마이그레이션 잔재 필드는 제외되며, 일부는 의미 단위로 재구성됩니다.
실전 예제 1단계 — 기본 SELECT로 구매 오더 헤더 조회
가장 단순한 시나리오부터 시작합니다. 특정 회사코드와 공급업체 조건으로 최근 30일간 등록된 구매 오더 헤더를 추출하는 ABAP 코드입니다.
REPORT zr_vendor_po_basic.
DATA: lt_vendor_po TYPE STANDARD TABLE OF i_purchaseorder,
lv_from_date TYPE dats.
lv_from_date = sy-datum - 30.
SELECT PurchaseOrder,
CompanyCode,
Supplier,
PurchaseOrderType,
DocumentCurrency,
CreationDate,
CreatedByUser,
PurchaseOrderDate
FROM i_purchaseorder
WHERE CompanyCode = '1010'
AND Supplier = '0000100214'
AND CreationDate >= @lv_from_date
ORDER BY CreationDate DESCENDING
INTO TABLE @lt_vendor_po.
LOOP AT lt_vendor_po INTO DATA(ls_po).
WRITE: / ls_po-purchaseorder,
ls_po-supplier,
ls_po-documentcurrency,
ls_po-purchaseorderdate.
ENDLOOP.
이 예제의 핵심은 EKKO를 직접 조회할 때와 비교해 필드명이 비즈니스 친화적이라는 점입니다. EBELN 대신 PurchaseOrder, LIFNR 대신 Supplier를 사용하므로 코드 가독성이 크게 향상됩니다.
실전 예제 2단계 — Consumption View 생성과 Association 활용
실무에서는 단순 SELECT를 넘어 자체 Consumption View를 만들어 Fiori Elements나 OData 서비스에서 활용하는 경우가 많습니다. 아래는 I_PurchaseOrder를 기반으로 공급업체 명칭과 회사코드 명칭을 함께 노출하는 Custom Consumption View 예시입니다.
@AbapCatalog.sqlViewName: 'ZCVENDORPOLIST'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Vendor PO Dashboard Consumption'
@OData.publish: true
@VDM.viewType: #CONSUMPTION
define view ZC_VendorPoOverview
as select from I_PurchaseOrder as PoHeader
association [0..1] to I_Supplier as _Supplier
on $projection.Supplier = _Supplier.Supplier
association [0..1] to I_CompanyCode as _CompanyCode
on $projection.CompanyCode = _CompanyCode.CompanyCode
association [0..*] to I_PurchaseOrderItem as _Items
on $projection.PurchaseOrder = _Items.PurchaseOrder
{
key PoHeader.PurchaseOrder,
PoHeader.CompanyCode,
_CompanyCode.CompanyCodeName as CompanyCodeText,
PoHeader.Supplier,
_Supplier.SupplierName as SupplierName,
PoHeader.PurchaseOrderType,
PoHeader.DocumentCurrency,
PoHeader.PurchaseOrderDate,
PoHeader.CreatedByUser,
cast( 0 as abap.curr( 23, 2 ) ) as TotalNetAmount,
_Supplier,
_CompanyCode,
_Items
}
이 코드의 학습 포인트는 다음과 같습니다. 첫째, I_PurchaseOrder는 이미 EKKO 기반으로 정규화되어 있어 별도 JOIN 없이 마스터 데이터에 접근할 수 있습니다. 둘째, I_Supplier와 I_CompanyCode 같은 다른 Basic View들과 Association을 맺어 텍스트 정보를 자연스럽게 끌어옵니다. 셋째, _Items Association을 노출하면 OData 서비스에서 $expand=_Items 형태로 헤더-아이템을 한 번에 조회할 수 있습니다.
실전 예제 3단계 — 운영 환경을 고려한 ABAP 클래스 설계
실무 코드는 단순 SELECT보다 더 견고한 에러 처리, 로깅, 성능 최적화가 필요합니다. 아래는 구매 오더 헤더 데이터를 추출해 JSON으로 직렬화하는 서비스 클래스 예시입니다.
CLASS zcl_vendor_po_reader DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_po_summary,
purchase_order TYPE i_purchaseorder-purchaseorder,
supplier TYPE i_purchaseorder-supplier,
company_code TYPE i_purchaseorder-companycode,
currency TYPE i_purchaseorder-documentcurrency,
po_date TYPE i_purchaseorder-purchaseorderdate,
END OF ty_po_summary,
tt_po_summary TYPE STANDARD TABLE OF ty_po_summary WITH EMPTY KEY.
METHODS: fetch_recent_orders
IMPORTING iv_company_code TYPE bukrs
iv_days_back TYPE i DEFAULT 30
iv_max_rows TYPE i DEFAULT 1000
RETURNING VALUE(rt_orders) TYPE tt_po_summary
RAISING cx_sy_open_sql_db.
PRIVATE SECTION.
METHODS: log_message
IMPORTING iv_text TYPE string.
ENDCLASS.
CLASS zcl_vendor_po_reader IMPLEMENTATION.
METHOD fetch_recent_orders.
DATA(lv_from_date) = CONV dats( sy-datum - iv_days_back ).
TRY.
SELECT FROM i_purchaseorder
FIELDS PurchaseOrder,
Supplier,
CompanyCode,
DocumentCurrency,
PurchaseOrderDate
WHERE CompanyCode = @iv_company_code
AND PurchaseOrderDate >= @lv_from_date
AND PurchasingDocumentDeletionCode = ''
ORDER BY PurchaseOrderDate DESCENDING
INTO CORRESPONDING FIELDS OF TABLE @rt_orders
UP TO @iv_max_rows ROWS.
log_message( |Fetched { lines( rt_orders ) } POs for { iv_company_code }| ).
CATCH cx_sy_open_sql_db INTO DATA(lx_db).
log_message( |DB error: { lx_db->get_text( ) }| ).
RAISE EXCEPTION lx_db.
ENDTRY.
ENDMETHOD.
METHOD log_message.
cl_demo_output=>write( iv_text ).
ENDMETHOD.
ENDCLASS.
이 패턴의 핵심 고려사항. 최대 행 수 제한으로 대량 조회 시 메모리 폭주를 방지합니다. 삭제 플래그 필터(PurchasingDocumentDeletionCode)를 추가해 논리 삭제된 PO를 제외합니다. 예외 처리를 통해 DB 단절이나 권한 오류 시 명확한 진단 정보를 제공합니다.
자주 마주치는 함정과 해결 방법
현장에서 I_PurchaseOrder를 사용할 때 흔히 발생하는 문제와 대응책을 정리합니다.
Q1. EKKO에는 있는 필드가 I_PurchaseOrder에서는 보이지 않습니다.
A. VDM Basic View는 의도적으로 일부 필드를 제외합니다. 정말 필요하다면 ADT에서 I_PurchaseOrder 소스를 열어 미노출 필드를 확인하고, Custom Extension View(EXTEND VIEW)를 만들어 추가하는 방식이 권장됩니다. EKKO를 직접 JOIN 하는 것은 가능한 한 피하세요.
Q2. SELECT 성능이 EKKO 직접 조회보다 느립니다.
A. SELECT *로 모든 필드를 가져오면 일부 가상 필드 계산 비용이 발생할 수 있으므로, 필요한 필드만 명시적으로 지정하는 것이 권장됩니다. WHERE 절에 인덱스 친화적인 키(CompanyCode, PurchaseOrder, Supplier)를 사용해야 합니다.
Q3. S/4HANA Cloud에서 I_PurchaseOrder를 직접 SELECT 할 수 없다고 합니다.
A. Cloud Public Edition에서는 Released Object 정책에 따라 직접 액세스가 제한될 수 있습니다. 이 경우 RAP Business Object의 Behavior나 공개된 OData API(API_PURCHASEORDER_PROCESS_SRV)를 통해 접근하는 것이 일반적입니다.
Q4. 삭제된 PO도 같이 조회됩니다.
A. I_PurchaseOrder는 EKKO의 모든 레코드를 노출하므로 논리 삭제 항목도 포함됩니다. PurchasingDocumentDeletionCode 필드로 필터링하거나, 릴리스 상태(PurchasingDocumentReleaseStatus)를 함께 확인해야 합니다.
이 글 이후 더 깊이 파고들 만한 주제
- I_PurchaseOrderItem: EKPO 기반 아이템 뷰. 헤더-아이템 연결 패턴의 핵심
- C_PurchaseOrderTP (RAP BO): RAP 기반 트랜잭션 처리를 위한 Composite View
- API_PURCHASEORDER_PROCESS_SRV: 외부 시스템 연동용 OData API
- CDS Extension: 표준 뷰를 확장해 커스텀 필드 추가하기
- Analytical Query: I_PurchaseOrder 기반으로 KPI 대시보드 구성
더 깊이 공부할 수 있는 외부 리소스
- help.sap.com — SAP S/4HANA 공식 도움말 포털
- help.sap.com — CDS Virtual Data Model (VDM) 가이드
- help.sap.com — Released Objects for S/4HANA
- SAP Business Accelerator Hub: PO Process API
- SAP Community 블로그 - CDS View 태그
댓글 0
아직 댓글이 없습니다.