ABAP

EINA EINE vs I_PurchasingInfo CDS #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 — 이 글에서 얻어갈 것

SAP S/4HANA에서 구매 정보 기록(Purchasing Info Record)을 조회할 때, 여전히 EINA와 EINE 테이블을 직접 조인하는 코드를 작성하고 계신가요? 이 글은 표준 CDS View인 I_PurchasingInfo를 활용해 테이블 직접 접근 없이 구매 정보 기록을 안정적으로 읽는 방법을 다루는 실전 예제입니다. S/4HANA의 Virtual Data Model(VDM) 관점에서 왜 CDS View 접근이 권장되는지, 그리고 실무 코드로 어떻게 전환하는지를 단계별로 살펴봅니다.

  • EINA/EINE 직접 조인 방식과 I_PurchasingInfo 방식의 구조적 차이 이해
  • 일반 데이터와 구매 조직 데이터를 Association으로 연결해 조회하는 문법 습득
  • 특정 공급업체의 자재별 구매 가격을 조회하는 ABAP 프로그램 작성
  • 에러 처리, 로깅, 테스트, 성능까지 고려한 프로덕션 수준 코드 구성

사전에 알아두면 좋은 지식

이 글은 advanced 난이도로, ABAP Open SQL의 기본 문법(SELECT, INTO TABLE, WHERE)과 CDS View의 기초 개념(DDL, Association, Annotation)을 이미 알고 있다는 전제로 진행합니다. MM(자재관리) 모듈에서 구매 정보 기록이 어떤 비즈니스 의미를 갖는지 — 즉 "특정 공급업체가 특정 자재를 어떤 조건으로 공급하는가"에 대한 마스터 데이터라는 점을 알고 있으면 이해가 훨씬 빠릅니다. ABAP Unit 테스트 경험이 있다면 3단계 예제까지 무리 없이 따라올 수 있습니다.

환경 · 버전 · 준비물

이 글의 코드는 다음 환경을 기준으로 작성했습니다.

  • SAP S/4HANA 2022 이상 (On-Premise) 또는 SAP S/4HANA CloudI_PurchasingInfo는 S/4HANA VDM의 Basic Interface View로 제공됩니다.
  • ABAP Development Tools(ADT) for Eclipse — CDS View의 정의와 Association 구조를 확인하려면 ADT의 Data Definition 뷰어가 사실상 필수입니다. SE11로는 CDS 소스를 볼 수 없습니다.
  • 권한: 구매 정보 기록 조회 권한(일반적으로 M_EINF_* 계열 권한 객체)이 필요합니다. CDS View에는 DCL(Access Control)이 걸려 있어 권한이 없으면 결과가 조용히 필터링될 수 있습니다.

ADT에서 I_PurchasingInfo를 열어보려면 Open ABAP Development Object(Ctrl+Shift+A)에서 이름을 검색하면 됩니다. 릴리스 상태(Release Contract)는 시스템 버전에 따라 다르므로, Properties 탭에서 API State를 직접 확인하는 것이 권장됩니다.

핵심 개념 — EINA/EINE는 창고, CDS View는 진열대

구매 정보 기록은 물리적으로 두 테이블에 나뉘어 저장됩니다. EINA는 일반 데이터(자재, 공급업체, 정보 기록 번호 등 조직 독립적 정보)를, EINE는 구매 조직 데이터(구매 조직, 플랜트, 순가격, 가격 단위 등)를 담습니다. 클래식 ABAP에서는 이 둘을 INNER JOIN으로 묶고, 삭제 플래그(LOEKZ)를 수동으로 걸러내는 코드를 반복 작성해야 했습니다.

비유하자면 EINA/EINE 직접 접근은 창고에 직접 들어가 박스를 뜯는 방식입니다. 어떤 박스가 폐기 대상인지(삭제 플래그), 어떤 박스가 세트로 묶이는지(조인 조건)를 전부 개발자가 기억해야 합니다. 반면 I_PurchasingInfo정리된 진열대입니다. SAP가 시맨틱한 영문 필드명, 삭제 처리 로직, 연관 데이터로의 경로(Association)를 미리 모델링해 두었습니다.

구조를 도식화하면 다음과 같습니다.

-- 개념 구조 (실제 소스의 재구성)
I_PurchasingInfo                     -- EINA 기반 (일반 데이터)
 ├─ PurchasingInfoRecord             -- 정보 기록 번호 (EINA-INFNR)
 ├─ Supplier                         -- 공급업체 (EINA-LIFNR)
 ├─ Material                         -- 자재 (EINA-MATNR)
 ├─ MaterialGroup, SupplierMaterialNumber ...
 └─ _PurgInfoRecdOrgPlantData        -- Association → EINE 기반 뷰
      ├─ PurchasingOrganization      -- 구매 조직 (EINE-EKORG)
      ├─ Plant                       -- 플랜트 (EINE-WERKS)
      ├─ PurchasingInfoRecordCategory
      ├─ NetPriceAmount              -- 순가격 (EINE-NETPR)
      ├─ OrderPriceUnit              -- 발주 가격 단위
      └─ Currency

핵심은 일반 데이터와 조직 데이터가 Association으로 느슨하게 연결되어 있다는 점입니다. 조인을 직접 쓰지 않고 경로 표현식(\_PurgInfoRecdOrgPlantData)으로 필요한 시점에만 조직 데이터를 끌어오므로, 옵티마이저가 불필요한 조인을 제거(join pruning)할 여지가 생깁니다. 또한 CDS View 이름과 필드명은 릴리스된 API 계약의 일부이므로, 향후 SAP가 내부 테이블 구조를 바꾸더라도 코드 호환성이 유지될 가능성이 높습니다. 한 가지 주의할 점은, EINE의 NetPriceAmount는 최종 구매 문서 기준의 가격 스냅샷에 가깝고, 유효 기간별 정밀한 가격은 조건(Condition) 테이블 기반의 별도 가격 결정 로직을 따른다는 것입니다. 단순 조회·리포트 용도라면 이 필드로 충분한 경우가 일반적입니다.

실전 코드 3단계

특정 공급업체가 공급하는 자재 목록과 정보 기록 번호를 가져오는 최소 코드입니다. 클래식 방식과 비교해 보겠습니다.

" (구식) EINA + EINE 직접 조인 — 더 이상 권장되지 않는 패턴
SELECT a~infnr, a~matnr, e~ekorg, e~netpr
  FROM eina AS a
  INNER JOIN eine AS e ON e~infnr = a~infnr
  WHERE a~lifnr = @lv_vendor
    AND a~loekz = @space
    AND e~loekz = @space
  INTO TABLE @DATA(lt_old_way).

" (권장) I_PurchasingInfo + Association 경로 표현식
SELECT FROM I_PurchasingInfo AS info
  FIELDS info~PurchasingInfoRecord,
         info~Supplier,
         info~Material,
         info~\_PurgInfoRecdOrgPlantData-PurchasingOrganization AS PurgOrg,
         info~\_PurgInfoRecdOrgPlantData-NetPriceAmount         AS NetPrice,
         info~\_PurgInfoRecdOrgPlantData-Currency               AS Currency
  WHERE info~Supplier = @lv_vendor
  INTO TABLE @DATA(lt_info_records).

삭제 플래그 필터링을 직접 쓰지 않아도 되는 이유는, VDM 뷰가 일반적으로 유효 레코드만 노출하도록 모델링되어 있기 때문입니다. 다만 시스템 버전에 따라 IsDeleted 유사 필드가 노출되는 경우가 있으니 ADT에서 필드 목록을 한 번 확인하는 것이 안전합니다.

구매팀이 "공급업체 X의 자재별 최신 순가격 목록"을 요청했다고 가정합니다. 결과 없음, 권한 부족 같은 실무 상황을 처리하는 클래스를 작성합니다.

CLASS zcl_vendor_price_report DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_price_line,
             info_record TYPE ebeln,
             material    TYPE matnr,
             purg_org    TYPE ekorg,
             plant       TYPE werks_d,
             net_price   TYPE bprei,
             currency    TYPE waers,
           END OF ty_price_line,
           tt_price_lines TYPE STANDARD TABLE OF ty_price_line WITH EMPTY KEY.

    METHODS get_prices_for_vendor
      IMPORTING iv_supplier      TYPE lifnr
                iv_purg_org      TYPE ekorg OPTIONAL
      RETURNING VALUE(rt_result) TYPE tt_price_lines
      RAISING   zcx_purchasing_data_error.
ENDCLASS.

CLASS zcl_vendor_price_report IMPLEMENTATION.
  METHOD get_prices_for_vendor.
    SELECT FROM I_PurchasingInfo AS info
      INNER JOIN I_PurchasingInfoRecdOrgPlntData AS org
        ON org~PurchasingInfoRecord = info~PurchasingInfoRecord
      FIELDS info~PurchasingInfoRecord AS info_record,
             info~Material             AS material,
             org~PurchasingOrganization AS purg_org,
             org~Plant                  AS plant,
             org~NetPriceAmount         AS net_price,
             org~Currency               AS currency
      WHERE info~Supplier = @iv_supplier
        AND ( org~PurchasingOrganization = @iv_purg_org
              OR @iv_purg_org IS INITIAL )
      ORDER BY info~Material, org~PurchasingOrganization
      INTO CORRESPONDING FIELDS OF TABLE @rt_result.

    IF rt_result IS INITIAL.
      MESSAGE e001(zmm_price) WITH iv_supplier INTO DATA(lv_msg).
      zcl_app_logger=>get_instance( )->log_warning(
        iv_object = 'ZMM_PRICING' iv_text = lv_msg ).
      RAISE EXCEPTION TYPE zcx_purchasing_data_error
        EXPORTING textid = zcx_purchasing_data_error=>no_records_found.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

여기서는 Association 경로 대신 EINE 기반 뷰(I_PurchasingInfoRecdOrgPlntData)를 명시적으로 조인했습니다. 조직 데이터의 여러 필드를 한꺼번에 가져올 때는 경로 표현식을 반복하는 것보다 명시적 조인이 읽기 쉽고, 실행 계획도 동일하게 최적화되는 것이 일반적입니다. "결과 없음"을 단순히 빈 테이블로 반환하지 않고 예외로 올리는 이유는, 호출자가 권한 필터링으로 비었는지, 데이터가 실제로 없는지를 로그로 추적할 수 있게 하기 위함입니다.

반복 사용되는 조회 로직은 커스텀 CDS View로 래핑하면 재사용성과 테스트 용이성이 올라갑니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Vendor Material Price List'
define view entity ZI_VendorMaterialPrice
  as select from I_PurchasingInfo as Info
  association [0..*] to I_PurchasingInfoRecdOrgPlntData as _OrgData
    on _OrgData.PurchasingInfoRecord = Info.PurchasingInfoRecord
{
  key Info.PurchasingInfoRecord,
  key _OrgData.PurchasingOrganization,
      Info.Supplier,
      Info.Material,
      _OrgData.Plant,
      @Semantics.amount.currencyCode: 'Currency'
      _OrgData.NetPriceAmount,
      _OrgData.Currency,
      _OrgData.OrderPriceUnit
}
where Info.Supplier is not initial

테스트는 CDS Test Double Framework로 DB 의존 없이 검증합니다.

CLASS ltc_vendor_price DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    CLASS-DATA go_env TYPE REF TO if_cds_test_environment.
    CLASS-METHODS class_setup.
    METHODS returns_price_for_vendor FOR TESTING.
ENDCLASS.

CLASS ltc_vendor_price IMPLEMENTATION.
  METHOD class_setup.
    go_env = cl_cds_test_environment=>create(
      i_for_entity = 'ZI_VENDORMATERIALPRICE' ).
  ENDMETHOD.

  METHOD returns_price_for_vendor.
    DATA(lt_stub) = VALUE ztt_vendor_price(
      ( purchasinginforecord = '5300001234'
        supplier = 'VEND_KR01' material = 'RAW-STEEL-10'
        purchasingorganization = '1010'
        netpriceamount = '1250.00' currency = 'KRW' ) ).
    go_env->insert_test_data( i_data = lt_stub ).

    SELECT * FROM zi_vendormaterialprice
      WHERE supplier = 'VEND_KR01'
      INTO TABLE @DATA(lt_actual).

    cl_abap_unit_assert=>assert_equals(
      act = lines( lt_actual ) exp = 1 ).
  ENDMETHOD.
ENDCLASS.

프로덕션 관점의 체크포인트는 세 가지입니다. 성능: SELECT * 대신 필요한 필드만 명시하고, 루프 내 SELECT 대신 한 번의 집합 조회로 처리합니다. 보안: 커스텀 뷰에 @AccessControl.authorizationCheck: #CHECK와 DCL을 적용해 표준 뷰의 권한 체계를 우회하지 않도록 합니다. 안정성: 릴리스되지 않은 내부 뷰(P_* 계열)는 참조하지 말고, I_* 인터페이스 뷰만 사용하는 것이 업그레이드 안전성 측면에서 권장됩니다.

흔한 실수와 트러블슈팅 FAQ

Q1. 조회 결과가 SE16의 EINA 건수보다 적게 나옵니다.

가장 흔한 원인은 두 가지입니다. 첫째, CDS View의 DCL이 사용자 권한에 따라 행을 필터링하고 있을 수 있습니다. ADT의 Access Control 문서를 확인하고, 임시로 권한 이슈인지 판별하려면 권한이 넓은 계정으로 비교 조회해 보세요. 둘째, 삭제 플래그가 걸린 레코드가 뷰에서 제외되었을 수 있습니다. 이는 버그가 아니라 의도된 모델링입니다.

Q2. NetPriceAmount가 ME13에서 보는 조건 가격과 다릅니다.

EINE 기반의 순가격은 마지막 구매 문서에서 갱신된 스냅샷 성격이 강합니다. 유효 기간별 조건 가격이 필요하면 가격 조건 레코드를 노출하는 별도 CDS 뷰(가격 조건 계열 I_* 뷰)나 가격 결정 API를 사용해야 합니다. 리포트 요구사항이 "현재 유효 조건가"인지 "최종 발주가"인지를 먼저 확인하는 것이 중요합니다.

Q3. Association 경로 표현식에서 구문 오류가 발생합니다.

ABAP SQL에서 경로 표현식은 info~\_AssocName-Field처럼 백슬래시(\)로 시작해야 하며, 이 문법은 특정 NetWeaver/S/4 릴리스 이상에서만 지원됩니다. 구버전 커널이라면 명시적 조인으로 대체하세요. 또한 [0..*] 카디널리티 Association을 경로로 접근하면 결과 행이 늘어날 수 있어(1:N 팬아웃), 집계 리포트에서는 중복 합산에 주의해야 합니다.

Q4. 커스텀 뷰 활성화 시 "authorization check missing" 경고가 뜹니다.

@AccessControl.authorizationCheck 어노테이션이 누락된 경우입니다. 조회 전용 내부 뷰라도 #NOT_REQUIRED를 명시적으로 선언하거나, 표준 권한을 상속하는 DCL을 작성해 경고를 해소하는 것이 일반적입니다.

클린 코어 전략과 I_PurchasingInfo의 위치

SAP가 S/4HANA에서 강조하는 "클린 코어(Clean Core)" 전략의 핵심은, 커스터마이징을 SAP 표준 API와 확장 포인트 위에서만 하고 내부 구현 세부사항에 의존하지 않는 것입니다. EINA/EINE 직접 접근은 이 전략의 정반대입니다. SAP는 이런 내부 테이블의 구조를 언제든 변경할 수 있으며, 변경 시 SAP Note 등 사전 공지 의무가 없습니다. 반면 I_PurchasingInfo처럼 릴리스된 C1 API 계약 아래 있는 CDS View는, SAP가 해당 릴리스 주기 동안 하위 호환성을 보장합니다.

실무적으로 클린 코어 전환 체크리스트에서 구매 정보 기록 관련 항목을 점검할 때, EINA/EINE 직접 SELECT가 발견되면 우선순위를 높여 I_PurchasingInfo로 교체하는 것이 권장됩니다. 특히 S/4HANA 클라우드 환경에서는 내부 테이블 직접 접근 자체가 제한될 수 있어, 마이그레이션 이전에 미리 전환해 두는 것이 안전합니다.

다음으로 확장할 주제

이 글에서 다룬 조회 패턴을 익혔다면, 다음 주제로 확장해 보시길 권합니다. 첫째, 구매 정보 기록의 생성·수정 — OData API(API_INFORECORD_PROCESS_SRV)나 BAPI를 통한 쓰기 작업으로, 읽기와는 전혀 다른 트랜잭션 처리가 필요합니다. 둘째, 가격 조건 CDS 뷰를 활용한 유효 기간별 가격 이력 분석. 셋째, ZI_VendorMaterialPrice 같은 커스텀 뷰 위에 Consumption View와 OData 서비스를 얹어 Fiori 리스트 리포트로 노출하는 RAP 기반 확장입니다. EINA/EINE에서 CDS로의 전환은 단순한 문법 교체가 아니라, S/4HANA 클린 코어 전략의 출발점이라는 관점으로 접근하면 좋습니다.

댓글 0

아직 댓글이 없습니다.