본문 바로가기
MM 모듈

[SAP ABAP] 수불부 데이터 집계 SELECT — MCHB·MSKA·MSKU·MSLB·MKOL 5종 통합 (CORRESPONDING BASE MAPPING)

by Song.sh 2026. 5. 19.

SAP MM 모듈에서 "수불부(자재 입출고 장부) 데이터를 집계해 달라" 는 요청은 ABAP 개발자가 가장 자주 받는 의뢰 중 하나입니다. 어려운 점은 SQL 한두 줄이 아니라, 재고가 한 테이블에 모여 있지 않다 는 것입니다. 일반 재고는 MARD, 배치 재고는 MCHB, 판매오더 재고는 MSKA, 고객 위탁은 MSKU 같이 재고의 성격마다 테이블이 따로 있고, 자재이동(입출고) 이력은 또 MKPF + MSEG 별도입니다.

 

업무 요건에 따라 5~6개 재고 테이블에서 동일 구조로 데이터를 뽑아 하나로 합쳐야 하는데, 매번 LOOP AT ... APPEND 로 한 줄씩 옮기면 코드가 길어지고 가독성도 떨어집니다. ABAP 7.40 부터 도입된 CORRESPONDING #( base ( ... ) ... MAPPING ... ) 표현식이 이 문제를 깔끔하게 해결해 줍니다.

 

이 글은 수불부 SELECT 에 자주 쓰이는 테이블 22종을 그룹별로 정리하고, 재고 5종 테이블을 LOOP 없이 CORRESPONDING + MAPPING 하나로 한 ITAB 에 합치는 실무 패턴을 담은 메모입니다. 자재이동 이력(MKPF·MSEG) 조회와 자재마스터·특성 조인까지 한 흐름으로 정리했습니다.


핵심 — 수불부 관련 테이블 그룹별 분류

수불부 데이터는 성격에 따라 4개 그룹으로 나뉩니다.

그룹 주요 테이블 역할
자재 마스터 MARA · MAKT · MARC · MARD · MBEW · MBEWH 자재 일반·내역·플랜트·저장위치·평가 정보 + 가용/품질 재고
배치 + 특성 MCHA · MCH1 · MCHB · AUSP · CABN 배치 정보·특성값·클래스 (배치 관리 자재용)
특별 재고 MSKA · MSKU · MSLB · MKOL 판매오더 / 고객 위탁 / 공급업체 특별 / 위탁 재고
자재이동 (이력) MKPF · MSEG · T156W · S031 · S034 입출고 헤더·항목 + 이동유형 마스터 + LIS 변경이력

핵심 한 줄: "현재 재고" 는 5개 테이블에서 따로 SELECT 해 합치고, "기간 내 입출고" 는 MKPF + MSEG 에서 조인. 합치는 작업은 LOOP 없이 CORRESPONDING + MAPPING 한 줄로 끝납니다.


1단계 — 재고가 5개 테이블에 분산된 이유

SAP 가 재고를 한 테이블에 모으지 않은 이유는 재고의 "주인"이 다르기 때문 입니다.

테이블 담는 재고 대표 수량 필드
MARD 일반 저장위치 재고 LABST(가용)·INSME(품질)
MCHB 배치 단위 재고 CLABS·CINSM·CUMLM
MSKA 판매오더 재고 (특별 재고 지시자 E) KALAB·KAINS·KASPE
MSKU 고객 위탁 재고 (W) KULAB·KUINS
MSLB 공급업체 특별 재고 (O — 사외 외주) LBLAB·LBINS
MKOL 공급업체 위탁 재고 (K) SLABS·SINSM·SSPEM

업무 환경에 따라 MSLB·MKOL 은 실제 데이터가 없는 경우도 흔합니다(특별 재고 미사용 시). SELECT 결과가 0건이면 자연스럽게 통합 테이블에 영향을 주지 않고 넘어갑니다.


2단계 — 통합 구조체 정의

5개 테이블의 결과를 한 ITAB 으로 합치려면 모든 필드를 담을 수 있는 상위 구조체 가 필요합니다. 실무 패턴은 CBO 테이블(예: ZTXX0123) 의 INCLUDE 위에 추가 필드를 얹는 방식입니다.

DATA: BEGIN OF gs_main,
        mark TYPE c,
        INCLUDE STRUCTURE ztxx0123.   " 회사 표준 수불 집계 CBO
        DATA: mtart TYPE mara-mtart,       " 자재유형
              mtbez TYPE t134t-mtbez,      " 자재유형 내역
              matkl TYPE mara-matkl,       " 자재그룹
              wgbez TYPE t023t-wgbez,      " 자재 그룹 내역
              maktx TYPE makt-maktx,       " 자재 내역
              kname TYPE kna1-name1,       " 고객 내역
              lname TYPE lfa1-name1,       " 공급업체 내역
              wname TYPE t001w-name1,      " 플랜트 내역
              sotxt TYPE t148t-sotxt,      " 특별 재고 지시자 내역
              lgobe TYPE t001l-lgobe,      " 저장위치 내역
      END   OF gs_main.
DATA gt_main TYPE TABLE OF gs_main.

상위 구조체에 5개 재고 테이블의 모든 수량 필드(CLABS·KALAB·KULAB·...) 가 포함되어 있어야 합니다. CORRESPONDING 이 같은 이름 필드만 자동 매핑하기 때문입니다.


3단계 — LOOP 없이 통합: CORRESPONDING base MAPPING

핵심 패턴입니다. 각 재고 테이블을 SELECT 한 결과를 gt_main 에 누적 추가합니다.

DATA(lv_datum) = sy-datum - 1.

" 1) 배치 재고 SELECT
SELECT matnr, werks, lgort, charg,
       clabs, cumlm, cinsm, ceinm,
       cspem, cretm,
       @lv_datum AS esrda,
       CASE WHEN charg IS NOT NULL THEN 'MCHB' END AS table
  INTO TABLE @DATA(lt_mchb)
  FROM mchb
  WHERE clabs <> 0 OR cinsm <> 0 OR cspem <> 0 OR cumlm <> 0.

" 통합 테이블에 첫 합류
gt_main = CORRESPONDING #( lt_mchb MAPPING tabname = table ersda = esrda ).

여기서 CASE WHEN ... THEN 'MCHB' END AS table 은 "이 행이 어느 테이블에서 왔는지" 를 같이 담아 통합 후에도 추적 가능하게 만드는 트릭입니다. MAPPING tabname = table 로 별칭(tabletabname) 까지 한 줄에 처리합니다.

" 2) 판매오더 재고 SELECT
SELECT matnr, werks, lgort, charg, sobkz,
       vbeln, posnr, kalab, kains, kaspe,
       @lv_datum AS ersda,
       CASE WHEN charg IS NOT NULL THEN 'MSKA' END AS table
  INTO TABLE @DATA(lt_mska)
  FROM mska
  WHERE kalab <> 0 OR kains <> 0 OR kaspe <> 0.

" base 로 누적 추가 (이미 데이터 있으면 합치고, 없으면 새로)
IF gt_main IS NOT INITIAL.
  gt_main = CORRESPONDING #( BASE ( gt_main ) lt_mska MAPPING tabname = table ersda = ersda ).
ELSE.
  gt_main = CORRESPONDING #( lt_mska MAPPING tabname = table ersda = ersda ).
ENDIF.

BASE ( gt_main ) 의 의미: 기존 gt_main 데이터는 그대로 유지하면서 lt_mska 의 행을 뒤에 이어 붙입니다. BASE 없이 그냥 CORRESPONDING #( lt_mska ... ) 만 쓰면 기존 데이터가 통째로 덮어쓰여 사라집니다.

" 3) 고객 위탁 재고 (MSKU) — 같은 패턴
SELECT matnr, werks, charg, sobkz, kunnr,
       kulab, kuins, @lv_datum AS ersda,
       CASE WHEN charg IS NOT NULL THEN 'MSKU' END AS table
  INTO TABLE @DATA(lt_msku)
  FROM msku
  WHERE kulab <> 0 OR kuins <> 0.

IF gt_main IS NOT INITIAL.
  gt_main = CORRESPONDING #( BASE ( gt_main ) lt_msku MAPPING tabname = table ersda = ersda ).
ELSE.
  gt_main = CORRESPONDING #( lt_msku MAPPING tabname = table ersda = ersda ).
ENDIF.

" 4) 공급업체 특별재고 (MSLB) / 5) 공급업체 위탁재고 (MKOL) 도 동일 패턴
" ... (전체 코드 섹션 참조)

 

5번 반복하면 gt_main 한 ITAB 에 MCHB·MSKA·MSKU·MSLB·MKOL 모든 재고가 통합됩니다. tabname 컬럼으로 원본 추적이 가능하고, LOOP 한 줄도 사용하지 않습니다.


4단계 — 자재이동 이력: MKPF + MSEG

기간 내 입출고 이력은 MKPF(헤더) + MSEG(항목) 조인으로 가져옵니다.

SELECT m~mblnr,    " 자재이동 문서번호
       m~mjahr,    " 회계년도
       m~budat,    " 전기일
       s~zeile,    " 라인번호
       s~bwart,    " 이동유형 (101 입고, 261 출고 등)
       s~matnr,    " 자재
       s~werks,    " 플랜트
       s~lgort,    " 저장위치
       s~charg,    " 배치
       s~menge,    " 수량
       s~meins,    " 단위
       s~shkzg,    " H(출고) / S(입고) 표시
       s~dmbtr,    " 금액
       s~waers    " 통화
  INTO TABLE @DATA(lt_mvt)
  FROM mkpf AS m
  INNER JOIN mseg AS s ON s~mblnr = m~mblnr
                      AND s~mjahr = m~mjahr
  WHERE m~budat BETWEEN @p_from AND @p_to
    AND s~werks = @p_werks
    AND s~matnr IN @s_matnr.

주요 필드 의미:

  • BWART(이동유형) — 101(GR PO), 102(GR 취소), 261(자재출고), 311(저장위치 이동), 561(초기재고) 등
  • SHKZGS(차변·입고), H(대변·출고)
  • BUDAT(전기일) — 회계 기준 일자 / CPUDT(생성일) 와 다를 수 있음

기간 집계 시 전기일 BUDAT 기준 이 표준입니다. 생성일(CPUDT) 로 잡으면 후행 전기와 다른 결과가 나옵니다.


5단계 — 자재마스터·내역·특성 조인

수량만 가지고는 사용자에게 보여주기 어렵습니다. 자재내역·플랜트명·저장위치명 등 텍스트를 붙여야 합니다.

" 자재 내역 (가장 흔한 보강)
SELECT matnr, maktx FROM makt
  INTO TABLE @DATA(lt_makt)
  FOR ALL ENTRIES IN @gt_main
  WHERE matnr  = @gt_main-matnr
    AND spras = @sy-langu.

" gt_main 에 maktx 합치기
LOOP AT gt_main ASSIGNING FIELD-SYMBOL(<fs>).
  READ TABLE lt_makt INTO DATA(ls_makt)
    WITH KEY matnr = <fs>-matnr.
  IF sy-subrc = 0.
    <fs>-maktx = ls_makt-maktx.
  ENDIF.
ENDLOOP.

배치 특성값까지 필요하면 AUSP + CABN 조회를 추가합니다(이 글은 수량 집계 중심이라 생략).


흔히 빠뜨리는 함정

BASE 빼먹고 통합

gt_main = CORRESPONDING #( lt_msku MAPPING ... ).   " ❌ BASE 없음

BASE ( gt_main ) 가 빠지면 이전에 합쳐 둔 MCHB·MSKA 데이터가 통째로 사라집니다. 두 번째 SELECT 부터는 반드시 BASE 사용.

gt_main IS NOT INITIAL 분기 누락

첫 SELECT 결과가 비어 있을 수 있는 환경(특정 자재가 배치 관리 안 됨)에서는 두 번째 SELECT 가 첫 합류가 되어야 합니다. IS NOT INITIAL 분기를 두지 않으면 BASE ( gt_main ) 가 빈 테이블이라 결과가 비어 보일 수 있습니다.

수량 0 필터링 안 함

SELECT ... FROM mchb.   " ❌ WHERE 없음

재고 테이블에는 0 인 행이 많습니다. WHERE clabs <> 0 OR cinsm <> 0 OR ... 같이 의미 있는 수량만 가져오지 않으면 ITAB 이 비정상적으로 커집니다.

BUDAT vs CPUDT 혼동

MKPF 의 전기일(BUDAT) 과 생성일(CPUDT) 은 다를 수 있습니다. 회계 마감 후 후행 전기 시 생성일은 오늘이지만 전기일은 전월 말이 됩니다. 기간 수불부는 항상 BUDAT 기준.

FOR ALL ENTRIES 빈 테이블 체크

SELECT ... FOR ALL ENTRIES IN @gt_main WHERE ...

gt_main 이 비어 있으면 FOR ALL ENTRIES 가 WHERE 조건을 무시하고 전체 테이블 을 가져옵니다. 호출 전에 IF gt_main IS NOT INITIAL. 체크 필수.

S/4HANA 환경 권장

S/4HANA 의 자재이동 항목은 MATDOC 으로 통합되어 있으며 MSEG 는 호환성 뷰입니다. 신규 코드는 MATDOC 직접 조회나 CDS View(예: I_MaterialDocumentItem) 사용이 권장. 다만 본 글의 MSEG SELECT 도 S/4HANA 에서 그대로 동작합니다.


전체 코드 — 복사용 통합본

5개 재고 테이블 + 자재이동 + 자재내역 보강을 한 프로그램으로 정리한 예제. SE38 에 그대로 가져다 자재 범위·플랜트·기간만 채워 실행할 수 있습니다.

REPORT zr_stock_ledger_demo.

PARAMETERS:     p_werks TYPE t001w-werks OBLIGATORY.
SELECT-OPTIONS: s_matnr FOR mara-matnr,
                s_budat FOR mkpf-budat OBLIGATORY.

* === 1) 통합 구조체 ===
DATA: BEGIN OF gs_main,
        mark    TYPE c,
        tabname TYPE c LENGTH 4,
        ersda   TYPE syst-datum,
        matnr   TYPE mara-matnr,
        werks   TYPE t001w-werks,
        lgort   TYPE t001l-lgort,
        charg   TYPE mchb-charg,
        sobkz   TYPE mska-sobkz,
        vbeln   TYPE mska-vbeln,
        posnr   TYPE mska-posnr,
        kunnr   TYPE msku-kunnr,
        lifnr   TYPE mslb-lifnr,
        " 5개 재고 테이블의 수량 필드 모두 포함
        clabs   TYPE mchb-clabs, cumlm TYPE mchb-cumlm,
        cinsm   TYPE mchb-cinsm, ceinm TYPE mchb-ceinm,
        cspem   TYPE mchb-cspem, cretm TYPE mchb-cretm,
        kalab   TYPE mska-kalab, kains TYPE mska-kains, kaspe TYPE mska-kaspe,
        kulab   TYPE msku-kulab, kuins TYPE msku-kuins,
        lblab   TYPE mslb-lblab, lbins TYPE mslb-lbins,
        slabs   TYPE mkol-slabs, sinsm TYPE mkol-sinsm, sspem TYPE mkol-sspem,
        maktx   TYPE makt-maktx,
      END   OF gs_main.
DATA gt_main TYPE TABLE OF gs_main.

DATA(lv_datum) = sy-datum - 1.

* === 2) MCHB - 배치 재고 ===
SELECT matnr, werks, lgort, charg,
       clabs, cumlm, cinsm, ceinm, cspem, cretm,
       @lv_datum AS esrda,
       CASE WHEN charg IS NOT NULL THEN 'MCHB' END AS table
  INTO TABLE @DATA(lt_mchb)
  FROM mchb
  WHERE werks = @p_werks
    AND matnr IN @s_matnr
    AND ( clabs <> 0 OR cinsm <> 0 OR cspem <> 0 OR cumlm <> 0 ).

gt_main = CORRESPONDING #( lt_mchb MAPPING tabname = table ersda = esrda ).

* === 3) MSKA - 판매오더 재고 ===
SELECT matnr, werks, lgort, charg, sobkz, vbeln, posnr,
       kalab, kains, kaspe,
       @lv_datum AS ersda,
       CASE WHEN charg IS NOT NULL THEN 'MSKA' END AS table
  INTO TABLE @DATA(lt_mska)
  FROM mska
  WHERE werks = @p_werks
    AND matnr IN @s_matnr
    AND ( kalab <> 0 OR kains <> 0 OR kaspe <> 0 ).

IF gt_main IS NOT INITIAL.
  gt_main = CORRESPONDING #( BASE ( gt_main ) lt_mska MAPPING tabname = table ersda = ersda ).
ELSE.
  gt_main = CORRESPONDING #( lt_mska MAPPING tabname = table ersda = ersda ).
ENDIF.

* === 4) MSKU - 고객 위탁 재고 ===
SELECT matnr, werks, charg, sobkz, kunnr,
       kulab, kuins,
       @lv_datum AS ersda,
       CASE WHEN charg IS NOT NULL THEN 'MSKU' END AS table
  INTO TABLE @DATA(lt_msku)
  FROM msku
  WHERE werks = @p_werks
    AND matnr IN @s_matnr
    AND ( kulab <> 0 OR kuins <> 0 ).

IF gt_main IS NOT INITIAL.
  gt_main = CORRESPONDING #( BASE ( gt_main ) lt_msku MAPPING tabname = table ersda = ersda ).
ELSE.
  gt_main = CORRESPONDING #( lt_msku MAPPING tabname = table ersda = ersda ).
ENDIF.

* === 5) MSLB - 공급업체 특별재고 ===
SELECT matnr, werks, charg, sobkz, lifnr,
       lblab, lbins,
       @lv_datum AS ersda,
       CASE WHEN charg IS NOT NULL THEN 'MSLB' END AS table
  INTO TABLE @DATA(lt_mslb)
  FROM mslb
  WHERE werks = @p_werks
    AND matnr IN @s_matnr
    AND ( lblab <> 0 OR lbins <> 0 ).

IF gt_main IS NOT INITIAL.
  gt_main = CORRESPONDING #( BASE ( gt_main ) lt_mslb MAPPING tabname = table ersda = ersda ).
ELSE.
  gt_main = CORRESPONDING #( lt_mslb MAPPING tabname = table ersda = ersda ).
ENDIF.

* === 6) MKOL - 공급업체 위탁 재고 ===
SELECT matnr, werks, lgort, charg, sobkz, lifnr,
       slabs, sinsm, sspem,
       @lv_datum AS ersda,
       CASE WHEN charg IS NOT NULL THEN 'MKOL' END AS table
  INTO TABLE @DATA(lt_mkol)
  FROM mkol
  WHERE werks = @p_werks
    AND matnr IN @s_matnr
    AND ( slabs <> 0 OR sinsm <> 0 OR sspem <> 0 ).

IF gt_main IS NOT INITIAL.
  gt_main = CORRESPONDING #( BASE ( gt_main ) lt_mkol MAPPING tabname = table ersda = ersda ).
ELSE.
  gt_main = CORRESPONDING #( lt_mkol MAPPING tabname = table ersda = ersda ).
ENDIF.

* === 7) 자재 내역 보강 ===
IF gt_main IS NOT INITIAL.
  SELECT matnr, maktx FROM makt
    INTO TABLE @DATA(lt_makt)
    FOR ALL ENTRIES IN @gt_main
    WHERE matnr = @gt_main-matnr
      AND spras = @sy-langu.

  LOOP AT gt_main ASSIGNING FIELD-SYMBOL(<fs_m>).
    READ TABLE lt_makt INTO DATA(ls_makt)
      WITH KEY matnr = <fs_m>-matnr.
    IF sy-subrc = 0.
      <fs_m>-maktx = ls_makt-maktx.
    ENDIF.
  ENDLOOP.
ENDIF.

* === 8) 기간 자재이동 이력 (별도 ITAB) ===
SELECT m~mblnr, m~mjahr, m~budat,
       s~zeile, s~bwart, s~matnr, s~werks, s~lgort, s~charg,
       s~menge, s~meins, s~shkzg, s~dmbtr, s~waers
  INTO TABLE @DATA(lt_mvt)
  FROM mkpf AS m
  INNER JOIN mseg AS s ON s~mblnr = m~mblnr AND s~mjahr = m~mjahr
  WHERE m~budat IN @s_budat
    AND s~werks = @p_werks
    AND s~matnr IN @s_matnr.

* === 9) ALV 출력 (cl_salv_table 등으로) ===
" ... ALV 출력 코드 생략

수량 합계 컬럼이 많아 보이지만, 한 화면에 모든 재고 종류를 보여줘야 하는 경우 이 구조가 가장 직관적입니다. 특정 재고 종류만 보면 되는 환경이면 필드를 정리해 슬림화할 수 있습니다.


요약

단계 작업 핵심
1 통합 구조체 5개 재고 테이블의 모든 수량 필드 포함
2 재고 5종 SELECT MCHB · MSKA · MSKU · MSLB · MKOL — 수량 0 제외
3 LOOP 없이 합치기 CORRESPONDING #( BASE ( ... ) ... MAPPING ... )
4 자재이동 이력 MKPF + MSEG INNER JOIN · BUDAT 기준
5 텍스트 보강 MAKT(자재내역) · T001W(플랜트명) · T001L(저장위치명) 등 조인

수불부 SELECT 의 핵심은 "재고가 5~6개 테이블에 흩어져 있다" 는 사실을 인정하고, 같은 구조의 SELECT 를 반복하되 CORRESPONDING + BASE + MAPPING 으로 LOOP 없이 한 ITAB 에 모으는 패턴입니다. 한 번 만들어 두면 새로운 재고 테이블이 추가될 때도 SELECT 블록 하나만 복붙하면 끝납니다. tabname 컬럼으로 원본 추적이 살아 있어 디버깅도 쉽습니다.


Disclaimer — 이 포스트는 실무 정리 노트를 바탕으로 AI 보조로 정리되었습니다.

MARD · MCHB · MSKA · MSKU · MSLB · MKOL · MKPF · MSEG 는 SAP MM 모듈 표준 테이블로 ECC 6.0 / S/4HANA on-premise 환경에서 그대로 사용 가능합니다. S/4HANA 에서 자재이동 데이터는 MATDOC 으로 통합되어 있으며 MSEG 는 호환성 뷰로 유지됩니다. CORRESPONDING ... BASE ... MAPPING 표현식은 ABAP 7.40 SP08 이상에서 제공되며, 7.40 미만 환경에서는 LOOP AT ... ASSIGNING + MOVE-CORRESPONDING + APPEND 조합으로 동일 결과를 만들 수 있습니다. 기간 집계는 항상 MKPF-BUDAT(전기일) 기준이며, CPUDT(생성일) 와 다를 수 있으니 회계 마감 시점 후행 전기를 고려해 컬럼 선택에 주의하시기 바랍니다.