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 로 별칭(table → tabname) 까지 한 줄에 처리합니다.
" 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(초기재고) 등SHKZG—S(차변·입고),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(생성일) 와 다를 수 있으니 회계 마감 시점 후행 전기를 고려해 컬럼 선택에 주의하시기 바랍니다.