ABAP

아직도 MKPF MSEG 직접 JOIN? — 이렇게 바꾸세요 #shorts #SAP #ABAP

개요 및 도입

SAP S/4HANA에서 자재 이동(Goods Movement)은 입고, 출고, 전송, 재고조정 등 모든 물류 흐름의 근간이 되는 핵심 트랜잭션입니다. 전통적으로 ABAP 개발자는 MKPF(자재 문서 헤더)와 MSEG(자재 문서 항목)를 직접 JOIN하여 데이터를 조회해 왔지만, S/4HANA에서는 가상 데이터 모델(VDM)의 일부인 I_GoodsMovement CDS 뷰를 통해 의미론적으로 정제된 형태로 접근할 수 있습니다. 이 글에서는 해당 뷰의 구조, 필드 의미, 실전 활용 패턴을 다룹니다.

  • MKPF/MSEG와 I_GoodsMovement의 매핑 관계 이해
  • Movement Type, Plant, Storage Location 기반 필터링 패턴 습득
  • CDS 뷰 확장 및 어노테이션을 활용한 분석 시나리오 구현
  • 대용량 자재 문서 조회 시 성능 고려사항 적용

사전에 알아두면 좋은 지식

이 글의 내용을 원활히 따라오려면 ABAP CDS View 기본 문법(define view, association, @ObjectModel), MM 모듈의 자재 문서 개념(전기 일자, 이동 유형, 평가 영역), Open SQL 또는 ADBC를 통한 데이터 조회 경험이 있으면 좋습니다. ADT(ABAP Development Tools in Eclipse) 환경에서 CDS 작성 경험이 있다면 더욱 수월합니다.

환경 및 준비 사항

본 문서의 코드는 다음 환경 기준으로 검증되었습니다.

  • SAP S/4HANA 2022 (FPS02) 또는 2023 이상 (On-Premise / Private Cloud Edition)
  • ABAP Platform 2022 또는 ABAP for Cloud Development
  • ADT (ABAP Development Tools) 3.36 이상 + Eclipse 2023-09
  • SAP HANA DB 2.0 SPS07+
  • 권한: S_DEVELOP(개발), M_MSEG_LGO(자재 문서 조회), 평가 영역(WERKS) 접근 권한

참고로 SAP BTP, ABAP Environment(Steampunk)에서는 Release Contract가 'C1 - Use in Cloud Development'로 공개된 CDS 뷰만 사용 가능하므로, I_GoodsMovement의 릴리즈 상태를 ADT의 API State 탭에서 먼저 확인하는 것을 권장합니다.

핵심 개념과 동작 원리

전통적인 자재 문서 조회는 다음과 같은 구조를 가집니다. MKPF는 문서 번호(MBLNR), 회계연도(MJAHR), 전기일자(BUDAT), 입력일자(CPUDT), 사용자(USNAM) 등 헤더 정보를 보관하고, MSEG는 항목 번호(ZEILE), 자재(MATNR), 이동유형(BWART), 플랜트(WERKS), 보관장소(LGORT), 수량(MENGE), 금액(DMBTR) 등 항목 정보를 보관합니다. 두 테이블은 (MBLNR, MJAHR)을 키로 1:N 관계를 갖습니다.

I_GoodsMovement는 이 두 테이블을 단일 항목 레벨의 평면 뷰로 통합하여 제공하는 Basic Interface View입니다. 즉, MSEG 항목 한 건마다 MKPF의 헤더 속성이 함께 노출되는 구조이므로, 별도의 JOIN을 작성하지 않고도 "특정 기간 동안의 이동 항목"을 조회할 수 있습니다.

비유하자면 MKPF/MSEG는 "원자재 창고"이고 I_GoodsMovement는 "조립된 반제품"입니다. 개발자는 더 이상 원자재(JOIN 조건, 필드 매핑)를 직접 가공할 필요 없이 의미가 부여된 필드명(MaterialDocument, MaterialDocumentItem, GoodsMovementType, PostingDate, Plant)을 사용합니다.

또한 VDM 계층 구조상 I_GoodsMovement는 Basic 계층에 위치하며, 그 위에 Composite/Consumption 뷰(예: 분석용 큐브 뷰, Fiori 앱용 뷰)가 쌓이는 형태입니다. 따라서 직접 UI에 노출하기보다는 다른 CDS 뷰의 빌딩 블록으로 사용하는 것이 일반적입니다.

단계별 실전 예제

1단계: 기본 조회 — 특정 플랜트의 입고 항목 가져오기

가장 단순한 시나리오로, 특정 플랜트(예: 1010)에서 어제 발생한 입고(101) 이동 항목을 조회하는 Consumer CDS 뷰를 작성합니다.

@AbapCatalog.sqlViewName: 'ZIVPLNTINGM'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Plant Inbound Goods Movement'
define view ZI_PlantInboundGM
  as select from I_GoodsMovement
{
  key MaterialDocument,
  key MaterialDocumentItem,
      MaterialDocumentYear,
      PostingDate,
      Plant,
      StorageLocation,
      Material,
      GoodsMovementType,
      QuantityInBaseUnit,
      MaterialBaseUnit,
      CompanyCode,
      CreatedByUser
}
where Plant            = '1010'
  and GoodsMovementType = '101';

이 뷰를 작성한 후 ADT의 Data Preview(F8)로 결과를 확인하면, MKPF·MSEG JOIN 없이도 헤더 일자와 항목 수량이 한 행에 노출됩니다.

2단계: 실무 시나리오 — 이동유형 그룹별 집계와 예외 처리

실무에서는 입고/출고/전송을 동시에 집계해야 하는 경우가 많습니다. CASE 식으로 카테고리 컬럼을 생성하고, 핵심 데이터를 ABAP 클래스에서 호출하는 패턴입니다.

@AbapCatalog.sqlViewName: 'ZIGMCATEG'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'GM Category by Movement Type'
define view ZI_GoodsMovementCategorized
  as select from I_GoodsMovement
{
  key MaterialDocument,
  key MaterialDocumentItem,
      PostingDate,
      Plant,
      Material,
      GoodsMovementType,
      case
        when GoodsMovementType in ('101','103','105') then 'INBOUND'
        when GoodsMovementType in ('201','261','551') then 'OUTBOUND'
        when GoodsMovementType in ('301','311','313') then 'TRANSFER'
        else 'OTHER'
      end                                              as MovementCategory,
      QuantityInBaseUnit,
      MaterialBaseUnit,
      DebitCreditCode
}
where PostingDate >= $session.system_date - 30;

ABAP에서 이 뷰를 호출할 때는 로깅과 예외 처리를 반드시 함께 구성합니다.

CLASS zcl_gm_report DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_summary,
             plant            TYPE werks_d,
             movement_category TYPE string,
             total_qty        TYPE menge_d,
           END OF ty_summary,
           tt_summary TYPE STANDARD TABLE OF ty_summary WITH EMPTY KEY.

    METHODS get_summary
      IMPORTING iv_plant_from TYPE werks_d
                iv_plant_to   TYPE werks_d
      RETURNING VALUE(rt_data) TYPE tt_summary
      RAISING   cx_sy_open_sql_db.
ENDCLASS.

CLASS zcl_gm_report IMPLEMENTATION.
  METHOD get_summary.
    TRY.
        SELECT plant,
               MovementCategory AS movement_category,
               SUM( QuantityInBaseUnit ) AS total_qty
          FROM zi_goodsmovementcategorized
          WHERE plant BETWEEN @iv_plant_from AND @iv_plant_to
          GROUP BY plant, MovementCategory
          INTO TABLE @rt_data.
      CATCH cx_sy_open_sql_db INTO DATA(lx_err).
        MESSAGE lx_err TYPE 'E'.
        RAISE EXCEPTION lx_err.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

3단계: 프로덕션 적용 — 어소시에이션, 권한, 성능

실제 운영 환경에서는 자재 마스터 정보, 회사 코드, 거래 통화 등 부가 정보를 함께 보여줘야 하며, DCL(Data Control Language)을 통한 권한 적용이 필수입니다.

@AbapCatalog.sqlViewName: 'ZCGMENRICH'
@AccessControl.authorizationCheck: #CHECK
@VDM.viewType: #COMPOSITE
@Analytics.dataCategory: #FACT
@EndUserText.label: 'Enriched Goods Movement for Analytics'
define view ZC_GoodsMovementEnriched
  as select from I_GoodsMovement as GM
  association [0..1] to I_Material     as _Material
    on $projection.Material = _Material.Material
  association [0..1] to I_Plant        as _Plant
    on $projection.Plant    = _Plant.Plant
  association [0..1] to I_CompanyCode  as _CompanyCode
    on $projection.CompanyCode = _CompanyCode.CompanyCode
{
  key GM.MaterialDocument,
  key GM.MaterialDocumentItem,
      GM.MaterialDocumentYear,
      GM.PostingDate,
      GM.Plant,
      _Plant.PlantName,
      GM.StorageLocation,
      GM.Material,
      _Material.MaterialName,
      _Material.MaterialGroup,
      GM.GoodsMovementType,
      @Semantics.quantity.unitOfMeasure: 'MaterialBaseUnit'
      GM.QuantityInBaseUnit,
      GM.MaterialBaseUnit,
      GM.CompanyCode,
      _CompanyCode.CompanyCodeName,
      GM.CreatedByUser,
      _Material,
      _Plant,
      _CompanyCode
}
where GM.PostingDate >= '20260101';

운영 환경 적용 시 점검할 항목입니다.

  • 인덱스: MSEG의 BUDAT 보조 인덱스 또는 HANA 컬럼 스토어 통계 갱신 상태 확인. PostingDate 범위 필터는 일반적으로 셀렉티브해야 성능이 안정적입니다.
  • DCL: 평가 영역(Plant)·회사 코드 기반 권한 제한 뷰를 별도 작성하여 @AccessControl.authorizationCheck: #CHECK와 함께 적용 권장.
  • 패키지 분리: Basic/Composite/Consumption 계층을 별도 패키지에 배치하고 Release Contract를 명시.
  • 테스트: ABAP Unit + CDS Test Double Framework(cl_cds_test_environment)로 격리 테스트.

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

Q1. I_GoodsMovement 결과 건수가 MSEG 직접 조회와 다릅니다.
A. I_GoodsMovement는 일부 취소 문서(STORNO_DOC_ITEM)나 특정 거래 처리 카테고리를 제외/필터링하는 경우가 있습니다. ADT에서 뷰 정의를 열어 WHERE 조건과 어노테이션을 직접 확인하고, 필요하면 MSEG의 원본 키와 비교 검증하세요.

Q2. PostingDate 필터를 걸어도 너무 느립니다.
A. PostingDate 단독 필터는 매우 큰 범위가 되기 쉽습니다. Plant 또는 CompanyCode와 함께 조합하고, 가능하면 MaterialDocumentYear도 함께 지정하여 파티셔닝 효과를 활용하세요. HANA 환경에서도 컬럼 스토어 필터 푸시다운이 효율적으로 일어나도록 단일 필드 LIKE '%' 패턴은 피해야 합니다.

Q3. 단위(UOM) 변환이 제각각입니다.
A. QuantityInBaseUnit은 기본 단위 기준이지만, 발주/판매 단위로 보고하려면 별도 변환 함수 또는 @Semantics.quantity.unitOfMeasure 어노테이션을 사용해 UI 계층에서 변환을 수행하도록 설계하는 것이 일반적입니다.

Q4. Steampunk(Cloud)에서 I_GoodsMovement 사용 시 컴파일 오류가 발생합니다.
A. Released API 여부를 ADT의 API State 탭에서 확인합니다. 만약 'Not Released for Cloud Development'라면, 동등한 Released 뷰(예: I_MaterialDocumentItem 등)를 검색하여 대체하는 것을 권장합니다.

심화 학습 방향

이 글에서 다룬 패턴을 익혔다면, 다음 영역으로 확장해 보는 것을 권장합니다. (1) I_MaterialDocumentItem, I_MaterialDocumentHeader 같은 Released 뷰와 I_GoodsMovement의 차이 비교, (2) ABAP RAP를 활용한 Goods Movement 분석 OData 서비스 노출, (3) Embedded Analytics 큐브 뷰(@Analytics.dataCategory: #CUBE)로 확장하여 Fiori Analytical List Page에 연결, (4) Event-Driven 시나리오에서 MaterialDocumentPosted 비즈니스 이벤트와 결합한 실시간 모니터링.

더 살펴볼 자료

댓글 0

아직 댓글이 없습니다.