ABAP

ABAP 개발자 90%가 놓치는 CO 내부 오더 VDM #shorts #SAP #ABAP

▶ YouTube에서 보기

개요와 이 글에서 다루는 것

SAP S/4HANA의 관리회계(CO) 영역에서 내부 오더(Internal Order)는 마케팅 캠페인, 단발성 프로젝트, 임시 비용 집계 단위 등 다양한 목적으로 활용되는 핵심 오브젝트입니다. 전통적으로 ABAP 개발자들은 AUFK 테이블을 직접 SELECT하여 내부 오더 데이터를 추출했지만, S/4HANA 환경에서는 I_InternalOrder라는 가상 데이터 모델(VDM, Virtual Data Model) CDS 뷰가 표준 제공됩니다. 이 글에서는 AUFK 기반의 레거시 접근 방식과 I_InternalOrder 뷰를 통한 현대적 접근 방식을 비교하고, CO 비용 집계 시나리오에서 실전 활용법을 단계별로 다룹니다.

  • AUFK 테이블 구조와 내부 오더의 데이터 모델 이해
  • I_InternalOrder CDS 뷰의 필드 구성과 어소시에이션 활용
  • COSP/COSS 연계를 통한 비용 집계 데이터 조회 패턴
  • RAP 기반 OData 서비스에서 내부 오더 노출 방법
  • 권한 체크 이슈와 PFCG 역할 설계 고려사항

사전에 알아두면 좋은 것

이 글은 ABAP 개발 경험 1년 이상, CDS 뷰의 기본 문법(define view entity, association, annotation)에 익숙한 독자를 대상으로 합니다. CO 모듈의 기본 용어(원가요소, 원가센터, 내부 오더, 결산)와 SAP GUI 트랜잭션 KO01/KO02/KO03 사용 경험이 있으면 이해가 빠릅니다. ADT(ABAP Development Tools in Eclipse) 또는 BAS(Business Application Studio) 환경에서 CDS 뷰를 작성·활성화해본 경험을 전제로 합니다.

실행 환경과 준비물

이 글의 예제는 다음 환경에서 일반적으로 동작하는 것을 기준으로 작성되었습니다.

  • SAP S/4HANA 2022 또는 2023 (On-Premise) / S/4HANA Cloud Public Edition
  • ABAP Platform 2022 이상 (SAP_BASIS 757+)
  • ADT 3.36 이상이 설치된 Eclipse 2023-09+
  • CO 모듈이 활성화된 클라이언트, KO01에서 테스트 내부 오더 1건 이상 생성
  • 권한: S_DEVELOP (CDS 개발), S_ORD_OCM 또는 K_ORDER (내부 오더 조회), S_TABU_DIS (AUFK 직접 조회 시)
  • 샘플 CompanyCode 1010 (또는 데모 데이터의 1710) 기준으로 코드 작성

On-Premise 환경과 Cloud 환경에서 사용 가능한 CDS 뷰의 release contract가 다를 수 있으므로, 본인 시스템에서 I_InternalOrder의 릴리즈 상태(C1/C0)를 ADT의 Repository Tree에서 먼저 확인하는 것을 권장합니다.

AUFK와 I_InternalOrder 뷰의 동작 원리

AUFK 테이블은 SAP가 1992년 R/3 출시 이래 유지해온 "Order Master Data" 테이블입니다. 내부 오더뿐 아니라 PM 오더, PP 오더, QM 오더 등 SAP의 거의 모든 "오더" 오브젝트가 이 테이블에 헤더 정보를 저장합니다. 즉, AUFK는 다형성(polymorphic) 테이블이며, AUART(Order Type) 필드와 AUTYP(Order Category) 필드로 어떤 종류의 오더인지 구분합니다. CO 내부 오더는 일반적으로 AUTYP = '01'입니다.

비유하자면 AUFK는 동네 도서관의 "모든 자료" 카드 박스 같은 것입니다. 소설, 잡지, 영화 DVD가 한 박스에 섞여 있어 원하는 책 한 권을 찾으려면 매번 분류 라벨을 확인해야 합니다. 반면 I_InternalOrder는 "소설 코너"라는 별도 책장처럼, CO 내부 오더만 깔끔하게 정제되어 있고, 자주 함께 찾는 자료(원가센터, 회사코드, 통화 등)와의 어소시에이션이 미리 연결되어 있습니다.

I_InternalOrder는 내부적으로 AUFK를 베이스 테이블로 사용하지만, 다음과 같은 부가가치를 제공합니다.

  • AUTYP='01' 필터를 자동 적용하여 비-CO 오더를 제외
  • 비즈니스 친화적 필드명 노출 (AUFNR → InternalOrder, BUKRS → CompanyCode, KOSTV → ResponsibleCostCenter)
  • 표준 어소시에이션 제공 (_CompanyCode, _CostCenter, _Controllingarea, _OrderType, _Currency 등)
  • analytical/transactional annotation 부착으로 Fiori Elements 즉시 활용 가능
  • DCL(Data Control Language)을 통한 권한 자동 적용

기본 조회 예제부터 어소시에이션 활용까지

1단계로 가장 단순한 형태의 I_InternalOrder 조회를 살펴봅니다. 회사코드 1010에 속한 활성 내부 오더 목록을 추출하는 시나리오입니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'CO 내부 오더 기본 조회'
define view entity ZC_MarketingOrderList
  as select from I_InternalOrder as Ord
{
  key Ord.InternalOrder              as OrderID,
      Ord.InternalOrderDescription   as Description,
      Ord.CompanyCode                as CompanyCode,
      Ord.ResponsibleCostCenter      as RespCostCenter,
      Ord.ControllingArea            as COArea,
      Ord.OrderType                  as OrderType,
      Ord.InternalOrderIsBlocked     as IsBlocked,
      Ord.CreationDate               as CreatedOn
}
where Ord.CompanyCode = '1010'
  and Ord.InternalOrderIsBlocked = ' '

위 뷰는 AUFK를 직접 SELECT했다면 다음과 같이 작성해야 했던 코드와 본질적으로 같지만, 가독성이 압도적으로 좋습니다.

" 레거시 방식 - AUFK 직접 접근
SELECT aufnr, ktext, bukrs, kostv, kokrs, auart, loekz, erdat
  FROM aufk
  INTO TABLE @DATA(lt_orders)
  WHERE bukrs = '1010'
    AND autyp = '01'
    AND loekz = ' '.

레거시 방식의 문제는 (1) AUTYP 필터를 매번 명시해야 하고, (2) 텍스트(KTEXT)가 로그온 언어에 종속되어 다국어 시나리오에서 어색하며, (3) 필드명이 비즈니스 의미와 동떨어진 점입니다.

2단계로 어소시에이션과 비용 집계(ACDOCA) 연계를 추가합니다. 마케팅 캠페인 오더별 실적 비용을 집계하는 실전 시나리오입니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '캠페인 오더별 실적 비용 집계'
define view entity ZC_CampaignActualCost
  with parameters
    p_FiscalYear : abap.numc(4),
    p_Period     : abap.numc(3)
  as select from I_InternalOrder as Ord
  left outer join I_GLAccountLineItem as Posting
    on  Posting.AccountingDocumentItemType  = 'A'
    and Posting.CostObject                  = Ord.InternalOrder
    and Posting.FiscalYear                  = $parameters.p_FiscalYear
    and Posting.FiscalPeriod                = $parameters.p_Period
  association [1] to I_CompanyCode  as _Company
    on  $projection.CompanyCode = _Company.CompanyCode
  association [0..1] to I_CostCenter as _CostCenter
    on  $projection.RespCostCenter   = _CostCenter.CostCenter
    and $projection.COArea           = _CostCenter.ControllingArea
{
  key Ord.InternalOrder              as OrderID,
      Ord.InternalOrderDescription   as CampaignName,
      Ord.CompanyCode                as CompanyCode,
      Ord.ResponsibleCostCenter      as RespCostCenter,
      Ord.ControllingArea            as COArea,
      @Semantics.amount.currencyCode: 'TransactionCurrency'
      sum(Posting.AmountInTransactionCurrency)  as ActualCostAmount,
      Posting.TransactionCurrency               as TransactionCurrency,
      _Company,
      _CostCenter
}
group by
  Ord.InternalOrder,
  Ord.InternalOrderDescription,
  Ord.CompanyCode,
  Ord.ResponsibleCostCenter,
  Ord.ControllingArea,
  Posting.TransactionCurrency

실제 비용 집계는 ACDOCA(Universal Journal)를 베이스로 한 I_GLAccountLineItem을 활용하는 것이 S/4HANA의 권장 패턴입니다. 레거시 ECC에서 익숙했던 COSP/COSS Totals 테이블은 S/4HANA에서 호환성 뷰(compatibility view)로만 유지되며, 실제 데이터는 ACDOCA에서 읽어옵니다.

3단계로 RAP(RESTful ABAP Programming Model)에서 위 뷰를 OData로 노출합니다.

@EndUserText.label: 'Campaign Cost Service'
@OData.publish: true
define view entity ZC_CampaignCostQuery
  provider contract transactional_query
  as projection on ZC_CampaignActualCost
{
  key OrderID,
      CampaignName,
      CompanyCode,
      RespCostCenter,
      ActualCostAmount,
      TransactionCurrency,
      _Company,
      _CostCenter
}

비즈니스 로직 클래스에서 예외 처리와 Application Log를 남기는 패턴은 다음과 같습니다.

METHOD fetch_campaign_costs.
  TRY.
      SELECT FROM zc_campaignactualcost( p_fiscalyear = @iv_year,
                                          p_period     = @iv_period )
        FIELDS orderid, campaignname, actualcostamount, transactioncurrency
        WHERE companycode = @iv_company_code
        INTO TABLE @rt_result.

      IF sy-subrc <> 0.
        zcl_appl_logger=>warn(
          object    = 'ZCO_CAMPAIGN'
          subobject = 'COST_FETCH'
          message   = |No data for { iv_company_code } / { iv_year }/{ iv_period }| ).
      ENDIF.

    CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
      zcl_appl_logger=>error(
        object    = 'ZCO_CAMPAIGN'
        subobject = 'COST_FETCH'
        message   = lx_sql->get_text( ) ).
      RAISE EXCEPTION TYPE zcx_campaign_error
        EXPORTING previous = lx_sql.
  ENDTRY.
ENDMETHOD.

자주 마주치는 함정과 해결 방법

Q1. I_InternalOrder를 조회했는데 KO03에서는 보이는 오더가 나오지 않습니다.
가장 흔한 원인은 권한 부족입니다. I_InternalOrder는 DCL이 적용되어 있어, 사용자가 해당 회사코드(K_KOSTL) 또는 CO 영역(K_CCA)에 대한 PFCG 권한이 없으면 결과에서 자동으로 필터링됩니다. ADT에서 뷰를 활성화한 후 "Preview Data" 기능으로 직접 조회해보면, 본인 권한 범위 내 데이터만 보입니다. 해결책은 SU53 트랜잭션으로 누락 권한을 확인하거나, 디버깅 시 @AccessControl.authorizationCheck: #NOT_REQUIRED로 임시 우회(개발 시스템 한정)하는 것입니다.

Q2. AUTYP='01'만 필터링했는데도 PM 오더가 섞여 나옵니다.
AUFK 직접 SELECT 시 발생하는 전형적 실수입니다. AUTYP는 Order Category이며, 일부 PM/PP 오더가 동일 카테고리를 사용하는 경우가 있습니다. 정확한 CO 내부 오더만 추출하려면 OrderType(AUART) 필드의 Customizing 테이블 T003O를 함께 조인하여 KOART = 'CO' 조건을 추가해야 합니다. I_InternalOrder는 이 필터링을 내부적으로 처리하므로 권장됩니다.

Q3. COSP 직접 조회는 빠른데 ACDOCA 기반 뷰는 느립니다.
ACDOCA는 라인 아이템 테이블이라 데이터량이 COSP Totals 대비 10배 이상 클 수 있습니다. 대신 실시간성과 정합성이 보장됩니다. 성능 최적화를 위해 (1) Fiscal Year/Period를 반드시 필터로 사용, (2) CDS 뷰 레벨에서 GROUP BY로 집계, (3) HANA 자체의 Result Cache 활용을 위한 어노테이션 부착, (4) 대량 리포팅은 별도 ABAP CDS Table Function으로 분리하는 것이 일반적입니다.

기타 자주 발생하는 실수: I_InternalOrder의 InternalOrderIsBlocked 필드를 무시하고 조회 → 삭제 마킹된 오더가 결과에 포함됨. ControllingArea를 누락하고 CostCenter만 조인 → 동일 코스트센터 코드가 여러 CO 영역에 존재할 때 중복 발생. 통화 변환 없이 amount를 SUM → 회사코드 간 통화가 다르면 의미 없는 합계가 나옴.

실무 적용 체크리스트

  • AUFK 직접 SELECT → I_InternalOrder 마이그레이션 시 AUTYP 필터 제거 확인
  • DCL 적용 여부 확인: @AccessControl.authorizationCheck: #CHECK 명시
  • Amount 필드 반드시 @Semantics.amount.currencyCode 어노테이션 부착
  • S/4HANA Cloud 전환 시 release contract C1 뷰만 사용 (ADT Repository Tree 확인)
  • 대량 데이터 조회 시 Fiscal Year/Period 필터 필수 포함
  • ABAP Unit Test로 VDM 조회 성능 회귀 모니터링

마무리 — 현장에서 쓸 수 있는 판단 기준

I_InternalOrder를 쓸 것인지, AUFK를 직접 조회할 것인지 판단하는 기준은 단순합니다. S/4HANA 환경이라면 99% 상황에서 I_InternalOrder가 더 나은 선택입니다. 단, 다음 예외 상황에서는 AUFK 직접 접근이 필요할 수 있습니다: (1) 레거시 ECC 시스템 유지보수, (2) VDM에 노출되지 않은 저수준 필드 접근, (3) 성능 상 이유로 모든 오더 타입을 한 번에 조회해야 하는 아키텍처.

I_InternalOrder를 익혔다면 자연스럽게 확장할 영역들이 있습니다. I_CostCenter, I_ProfitCenter, I_WBSElement 등 다른 CO 오브젝트 VDM을 함께 다루어 통합 원가 리포트를 구성해볼 수 있습니다. Fiori Elements Analytical List Page와 연결하여 셀프서비스 BI 시나리오를 구현하거나, RAP의 managed behavior를 활용해 내부 오더의 결재/승인 워크플로우를 OData V4 기반으로 모델링할 수 있습니다.

댓글 0

아직 댓글이 없습니다.