ABAP

KEKO vs I_OrderCostEstimate — 원가 예산 조회 비교 #shorts #SAP #ABAP

개요 및 실무 활용 포인트

S/4HANA 환경에서 제조 원가 데이터를 조회할 때, 과거처럼 KEKO(원가 추산 헤더) 테이블을 직접 SELECT하는 방식은 유지보수성과 확장성 측면에서 여러 제약을 만납니다. 이 글은 SAP가 제공하는 표준 CDS 뷰 I_OrderCostEstimate를 활용해 KEKO 테이블에 직접 접근하지 않고도 Production Order 단위 원가 예산 데이터를 안정적으로 조회하는 방법을 다룹니다.

  • I_OrderCostEstimate 뷰의 구조와 KEKO/KEPH 대비 추상화 레이어의 의미
  • Production Order 및 Cost Center 기준 원가 예산 조회 실전 예제
  • 연관 뷰(I_ManufacturingOrder, I_CostCenter)와의 조인 패턴
  • 필터 최적화, HANA 푸시다운, Authorization 처리 노하우
  • KEKO 직접 접근 vs CDS 뷰 접근 방식 비교와 마이그레이션 전략

사전 이해가 필요한 개념

이 글은 ABAP CDS View의 기본 구문(@AbapCatalog.sqlViewName, association, define view)과 S/4HANA Virtual Data Model(VDM) 구조(Basic/Composite/Consumption 계층)에 익숙하다는 전제로 진행합니다. 또한 Product Cost Controlling(CO-PC) 영역의 원가 추산 흐름(KEKO 헤더 → KEPH 원가 성분 → CKIS 항목)과 Production Order 처리 프로세스에 대한 기초 지식이 있으면 이해가 훨씬 수월합니다.

환경 및 준비 사항

이 글의 예제는 다음 환경을 기준으로 검증했습니다. 릴리스별로 CDS 뷰 필드셋과 Annotation이 일부 다를 수 있으므로 자체 시스템의 SE11 또는 ADT의 CDS 뷰 뷰어에서 필드 존재 여부를 먼저 확인하는 것을 권장합니다.

  • SAP S/4HANA 2022 이상 (On-Premise) 또는 SAP S/4HANA Cloud 최신 릴리스
  • ABAP Development Tools (ADT) for Eclipse 3.30 이상
  • SAP HANA DB 2.0 SPS06 이상 (칼럼 스토어 최적화 활용)
  • 권한 오브젝트: C_KOSTL_KOA(Cost Center), C_AFKO_AWK(Order), K_CCA(CO Area) — DCL 설계 시 참조
  • 테스트 데이터: 상태(SysStatus)가 REL 또는 TECO인 Production Order와 그에 연결된 원가 추산 데이터

일반적으로 I_OrderCostEstimate는 Basic 계층 뷰로 분류되며, Cloud 에디션에서는 White-listed API로 승격되지 않은 경우도 있어 Released State(@API.state: 'RELEASED')를 사전에 확인해야 합니다.

핵심 개념: KEKO 없이 CDS 뷰로 접근한다는 것의 의미

전통적인 방식에서 원가 추산 데이터는 KEKO(헤더) — KEPH(원가 성분 총계) — CKIS(원가 항목 세부) 세 테이블을 조인해 얻어야 했습니다. 이 접근은 다음과 같은 부담을 만듭니다.

  • KEKO의 KALNR(원가 추산 번호)는 순수 내부 키로, Production Order(AUFNR)나 Material(MATNR)로 접근하려면 매번 KALO/KEKO 서브셀렉트를 걸어야 합니다.
  • Currency Type(WAERS), Valuation Variant(BWVAR), Costing Variant(KLVAR) 조합에 따라 여러 헤더가 존재하므로 "지금 유효한 하나"를 골라내는 필터 로직이 매번 반복됩니다.
  • KEKO 필드셋은 CO-PC 내부 관점이라 Business Object 층위(Order, Material, Plant)와 어휘가 어긋납니다.

I_OrderCostEstimate는 이 복잡성을 캡슐화한 Semantic Layer입니다. 비유하자면 KEKO/KEPH는 "설계도 원본"이고, I_OrderCostEstimate는 "현장 작업자용 요약도"에 해당합니다. 뷰 내부에서 이미 최신 유효 원가 추산 헤더가 선정되어 있고, 필드명이 ManufacturingOrder, CostingVariant, TotalCost처럼 VDM 표준 어휘로 매핑되어 있습니다.

추상화 레이어의 진짜 가치는 "필드 이름이 예뻐지는 것"이 아니라, SAP가 릴리스 업그레이드 시 하부 테이블 구조가 바뀌더라도 상위 뷰의 시그니처를 유지하여 소비자 코드를 보호한다는 점에 있습니다.

또한 I_OrderCostEstimate는 _ManufacturingOrder, _Plant, _CostingVariant 등의 Association을 이미 정의해두고 있어 별도 조인 없이 관련 마스터/문서 뷰로 확장할 수 있는 구조를 제공합니다.

실전 예제 1단계: 기본 조회 패턴

가장 간단한 형태로, 특정 Plant와 Costing Variant 조합에서 활성 상태 Production Order의 총 원가 예산을 조회하는 예제입니다. KEKO에 직접 접근하지 않고 Consumption View를 하나 만들어 구성 요소를 명시적으로 선택합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Production Order Cost Estimate - Basic'
@Metadata.allowExtensions: true
define view entity ZC_ProdOrderCostBasic
  as select from I_OrderCostEstimate as CostEst
  association [0..1] to I_ManufacturingOrder as _Order
    on $projection.ManufacturingOrder = _Order.ManufacturingOrder
{
  key CostEst.ManufacturingOrder,
      CostEst.Plant,
      CostEst.CostingVariant,
      CostEst.CostingDate,
      CostEst.CostEstimateCurrency,
      CostEst.TotalCost,
      CostEst.MaterialCost,
      CostEst.LaborCost,
      CostEst.OverheadCost,
      _Order.ManufacturingOrderType,
      _Order.MRPResponsible
}
where CostEst.CostEstimateStatus = 'KA'
  and CostEst.Plant is not initial

ABAP 코드에서는 이렇게 소비합니다.

SELECT ManufacturingOrder, TotalCost, CostEstimateCurrency
  FROM ZC_ProdOrderCostBasic
  WHERE Plant = @lv_plant
    AND CostingVariant = @lv_cost_variant
  INTO TABLE @DATA(lt_order_costs)
  UP TO 1000 ROWS.

실전 예제 2단계: Cost Center별 원가 예산 집계와 에러 처리

실무에서는 단일 오더가 아닌 Cost Center 또는 MRP Controller 단위로 원가 예산을 롤업하는 요구가 자주 발생합니다. 이 단계에서는 Composite View로 집계 로직을 캡슐화하고, 소비 계층에서 예외 처리와 로깅을 구현합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Cost Center Aggregated Order Cost Estimate'
define view entity ZI_CostCtrOrderEstimateAgg
  as select from I_OrderCostEstimate as CE
  association [1..1] to I_ManufacturingOrder as _Order
    on $projection.ManufacturingOrder = _Order.ManufacturingOrder
  association [0..1] to I_CostCenter as _CostCenter
    on  $projection.ControllingArea = _CostCenter.ControllingArea
    and $projection.CostCenter      = _CostCenter.CostCenter
{
  key _Order.MainWorkCenterInternalID as WorkCenterInternalID,
  key CE.ControllingArea,
  key _Order.CostCenter               as CostCenter,
  key CE.CostEstimateCurrency,
      sum(CE.TotalCost)     as TotalPlannedCost,
      sum(CE.MaterialCost)  as TotalMaterialCost,
      sum(CE.LaborCost)     as TotalLaborCost,
      count(distinct CE.ManufacturingOrder) as OrderCount
}
where CE.CostEstimateStatus = 'KA'
group by _Order.MainWorkCenterInternalID,
         CE.ControllingArea,
         _Order.CostCenter,
         CE.CostEstimateCurrency

ABAP Class에서 예외 처리와 애플리케이션 로그를 결합해 호출합니다.

METHOD get_cost_center_estimate.
  TRY.
      SELECT CostCenter, TotalPlannedCost, CostEstimateCurrency, OrderCount
        FROM ZI_CostCtrOrderEstimateAgg
        WHERE ControllingArea = @iv_co_area
          AND CostCenter IN @it_cost_center_range
        INTO TABLE @rt_result.

      IF rt_result IS INITIAL.
        MESSAGE i208(00) WITH 'No cost estimate data'
          INTO DATA(lv_msg).
        me->log_message( iv_severity = 'W'
                         iv_message  = lv_msg ).
      ENDIF.

    CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
      me->log_message( iv_severity = 'E'
                       iv_message  = lx_sql->get_text( ) ).
      RAISE EXCEPTION TYPE zcx_cost_estimate_error
        EXPORTING previous = lx_sql.
  ENDTRY.
ENDMETHOD.

여기서 주의할 점은 CostEstimateStatus가 뷰 내부에서 이미 필터되어 있지 않은 경우가 있다는 것입니다. SAP 릴리스마다 다르므로 반드시 뷰 정의를 열어 확인하고, 필요하면 Custom View에서 명시적으로 필터링해야 합니다.

실전 예제 3단계: 프로덕션 등급 — 성능, 테스트, 권한

운영 배포 단계에서는 세 가지가 중요합니다. HANA 푸시다운을 방해하지 않는 필터 설계, DCL(Data Control Language)로 권한 통제, 그리고 CDS Test Double Framework를 활용한 단위 테스트입니다.

@AccessControl.authorizationCheck: #CHECK
@Analytics.dataCategory: #CUBE
@VDM.viewType: #COMPOSITE
@EndUserText.label: 'Order Cost Estimate - Production Ready'
define view entity ZI_OrderCostEstProd
  with parameters
    P_DisplayCurrency : vdm_v_display_currency,
    P_ExchangeRateType: vdm_v_exchange_rate_type,
    P_KeyDate         : vdm_v_key_date
  as select from I_OrderCostEstimate as CE
{
  key CE.ManufacturingOrder,
  key CE.CostingVariant,
      CE.Plant,
      CE.ControllingArea,
      @Semantics.amount.currencyCode: 'DisplayCurrency'
      currency_conversion(
        amount             => CE.TotalCost,
        source_currency    => CE.CostEstimateCurrency,
        target_currency    => $parameters.P_DisplayCurrency,
        exchange_rate_date => $parameters.P_KeyDate,
        exchange_rate_type => $parameters.P_ExchangeRateType,
        error_handling     => 'SET_TO_NULL'
      )                              as TotalCostInDisplayCcy,
      $parameters.P_DisplayCurrency  as DisplayCurrency
}
where CE.CostEstimateStatus = 'KA'

DCL로 Cost Center 권한을 강제합니다.

@EndUserText.label: 'Authorization for Order Cost Estimate'
@MappingRole: true
define role ZI_OrderCostEstProd {
  grant select on ZI_OrderCostEstProd
    where (ControllingArea, CostCenter) =
      aspect pfcg_auth( K_CSKS, KOKRS, KOSTL, ACTVT = '03' );
}

CDS Test Double을 활용한 단위 테스트 뼈대입니다.

CLASS ltcl_order_cost_est DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    DATA: mo_cds_td TYPE REF TO if_cds_test_environment.

    METHODS: setup,
             teardown,
             test_total_cost FOR TESTING RAISING cx_static_check.
ENDCLASS.

CLASS ltcl_order_cost_est IMPLEMENTATION.
  METHOD setup.
    mo_cds_td = cl_cds_test_environment=>create(
                  i_for_entity = 'ZI_ORDERCOSTESTPROD' ).
  ENDMETHOD.

  METHOD teardown.
    mo_cds_td->clear_doubles( ).
  ENDMETHOD.

  METHOD test_total_cost.
    DATA lt_input TYPE STANDARD TABLE OF i_ordercostestimate.
    lt_input = VALUE #(
      ( manufacturingorder = '000000100200'
        costingvariant     = 'PPC1'
        totalcost          = '15000.00'
        costestimatecurrency = 'EUR'
        costestimatestatus = 'KA' ) ).
    mo_cds_td->insert_test_data( i_data = lt_input ).

    SELECT SINGLE TotalCostInDisplayCcy
      FROM ZI_OrderCostEstProd( P_DisplayCurrency = 'USD',
                                P_ExchangeRateType = 'M',
                                P_KeyDate = '20260101' )
      WHERE ManufacturingOrder = '000000100200'
      INTO @DATA(lv_cost).

    cl_abap_unit_assert=>assert_not_initial( lv_cost ).
  ENDMETHOD.
ENDCLASS.

성능 관점에서는 필터를 항상 키 필드에 우선 적용하고(Plant, ControllingArea, CostingVariant), UP TO n ROWS를 남용하지 말 것을 권장합니다. Composite View 위에 다시 DISTINCT를 걸면 HANA에서 hash aggregation이 발생해 인덱스를 못 타는 경우가 있으므로 프로파일링이 필요합니다.

자주 마주치는 함정과 대응

Q1. I_OrderCostEstimate에 데이터가 없다고 나옵니다.
가장 흔한 원인은 CostEstimateStatus 필터입니다. KEKO의 KKZMA 필드가 릴리스된 상태(KA)가 아닌 경우, 뷰가 내부적으로 이를 제외할 수 있습니다. SE16으로 KEKO에서 해당 KALNR의 상태를 먼저 확인하고, 필요하면 Custom View에서 상태 필터를 우회하도록 재정의해야 합니다.

Q2. Currency Conversion 결과가 NULL로 나옵니다.
currency_conversion 함수는 환율이 TCURR에 없거나 CostEstimateCurrency가 비어 있을 때 error_handling 옵션에 따라 다르게 동작합니다. SET_TO_NULL은 조용히 실패하므로, 개발 중에는 FAIL_ON_ERROR로 두어 문제를 조기에 노출시키는 것이 좋습니다.

Q3. KEKO를 직접 SELECT하던 기존 프로그램을 마이그레이션하면 성능이 떨어집니다.
이는 CDS 뷰의 오버헤드가 아니라, 필터 필드가 하부 조인에서 늦게 적용되기 때문입니다. WHERE 절에 뷰의 키 필드(ManufacturingOrder, Plant, CostingVariant)를 명시적으로 넣고, SQL Trace(ST05)로 생성된 Native SQL을 확인해 실제 푸시다운 여부를 검증하십시오. 필요하면 @Consumption.filter Annotation으로 소비자 도구에 필수 필터를 강제할 수 있습니다.

Q4. Cloud 시스템에서 I_OrderCostEstimate가 보이지 않습니다.
Cloud 에디션에서는 릴리스된 API만 사용 가능합니다. SAP API Business Hub에서 해당 뷰의 Released 상태를 확인하고, 그렇지 않다면 대체 CUBE 뷰(예: C_ProductionOrderCostAnalysis)를 사용하는 편이 낫습니다.

확장 학습 로드맵

이 글에서 다룬 접근 방식을 실전에 안착시켰다면 다음 주제로 확장해볼 것을 권합니다. 첫째, I_CostEstimateItemDetail 및 관련 CKIS 기반 뷰로 원가 성분(Cost Component) 단위 분석. 둘째, RAP(RESTful ABAP Programming Model)를 사용해 Order Cost Estimate 조회 서비스를 OData로 노출하고 Fiori Elements List Report로 시각화. 셋째, Embedded Analytics 관점에서 @Analytics.query: true를 활용한 KPI 대시보드 구축. 넷째, CDS View Entity의 새로운 문법(define view entity)으로 레거시 define view를 리팩터링하고 SQL View Name 의존성을 제거하는 작업입니다.

댓글 0

아직 댓글이 없습니다.