📖 개요: 왜 "그냥 12로 나누면" 안 되는가
자재를 낱개(EA)로 관리하면서 출하는 박스(KAR), 창고 적치는 팔레트(PAL)로 하는 것은 물류 현장의 일상입니다. 이때 ABAP 개발자가 가장 흔히 저지르는 실수가 lv_box = lv_ea / 12 같은 하드코딩 나눗셈입니다. 한 건만 보면 맞는 것 같지만, QUAN 타입의 소수점 절삭과 반올림이 수백 라인에 걸쳐 누적되면 재고 수량이 미세하게 어긋나고, MM/SD 표준 문서와 커스텀 리포트의 숫자가 서로 달라지는 사고로 이어집니다. 이 글은 SAP ECC 6.0 및 S/4HANA(온프레미스 2023 기준) 환경에서 자재마스터의 기준단위 환산 구조를 이해하고, 표준 변환 로직으로 안전하게 수량을 환산하는 방법을 다룹니다.
- 기준단위(Base UoM)와 대체단위(Alternative UoM)의 관계, MARM-UMREZ/UMREN 계수 구조 이해
- 직접 나눗셈 환산 시 반올림 오차가 누적되는 원리 파악
- MD_CONVERT_MATERIAL_UNIT, UNIT_CONVERSION_SIMPLE 등 표준 변환 활용법 습득
- 대량 처리 시 성능·정합성을 지키는 프로덕션 패턴 적용
📚 미리 알고 있으면 좋은 내용
중급 난이도의 글로, SELECT/내부 테이블/함수 모듈 호출 등 기본 ABAP 문법과 자재마스터(MM01/MM03)의 기초 단위 탭을 다뤄본 경험을 전제로 합니다. QUAN·UNIT 데이터 타입의 의미, 그리고 T006(측정 단위) 테이블이 무엇인지 대략 알고 있으면 이해가 빠릅니다. 클래스 기반 예제가 나오지만 ABAP OO 심화 지식은 필요하지 않습니다.
🔧 환경과 준비물
이 글의 코드는 다음 환경에서 동작을 전제로 작성되었습니다.
- SAP S/4HANA 온프레미스 2022/2023 또는 SAP ECC 6.0 EhP8 — MARA/MARM 테이블과 예시 함수 모듈은 두 환경 모두 존재합니다
- ABAP 7.4 이상 문법(인라인 선언, 신규 Open SQL) 사용 — 구버전에서는 선언부만 풀어 쓰면 동일하게 동작합니다
- 테스트용 자재 1건: 기준단위 EA, 대체단위 KAR(박스)=12 EA, PAL(팔레트)=144 EA로 MM02에서 미리 등록
- SE37에서 MD_CONVERT_MATERIAL_UNIT 단독 테스트 권한, SE16N으로 MARM 조회 권한
참고로 S/4HANA 퍼블릭 클라우드(ABAP Environment)에서는 고전 함수 모듈 대신 릴리스된 CDS 뷰 I_ProductUnitOfMeasure를 사용하는 방식이 일반적으로 권장되며, 이 글 후반에 간단히 언급합니다.
💡 핵심 개념: 기준단위는 "환율의 기축통화"다
SAP의 단위 환산 구조는 환율 계산과 비슷합니다. 원화→유로를 직접 환산하지 않고 달러(기축통화)를 거치듯, SAP은 모든 대체단위를 기준단위(MARA-MEINS)를 거쳐 환산합니다. 자재마스터의 대체단위 정보는 MARM 테이블에 저장됩니다.
| 필드 | 의미 | 예시 (박스 12개입) |
|---|---|---|
| MARM-MATNR | 자재번호 | FG-1001 |
| MARM-MEINH | 대체 단위 | KAR |
| MARM-UMREZ | 환산 분자 (기준단위 수량) | 12 |
| MARM-UMREN | 환산 분모 (대체단위 수량) | 1 |
환산 공식은 다음과 같습니다.
기준단위 수량 = 대체단위 수량 × UMREZ ÷ UMREN
대체단위 수량 = 기준단위 수량 × UMREN ÷ UMREZ
핵심은 UMREZ/UMREN이 정수 분수 쌍이라는 점입니다. "3 EA = 2 KG"처럼 비율이 나누어떨어지지 않는 경우에도 소수(0.6666…)로 저장하지 않고 UMREZ=2, UMREN=3이라는 정확한 분수로 보관합니다. 즉 데이터베이스 레벨에서는 반올림 오차가 존재하지 않습니다. 오차는 오직 개발자가 중간 결과를 소수로 잘라서 저장하는 순간에 발생합니다.
예를 들어 1000 EA를 12개입 박스로 직접 나누면 83.3333… 인데, QUAN 타입(소수 3자리)에 담는 순간 83.333으로 절삭됩니다. 이를 다시 EA로 되돌리면 999.996 EA — 0.004 EA가 증발합니다. 배송 라인 500건이 각각 이런 왕복 환산을 거치면 오차는 수 EA 단위로 누적되고, MB52 재고와 커스텀 리포트가 어긋나기 시작합니다. 흐름을 도식화하면 이렇습니다.
[잘못된 경로] EA → (÷12, 소수 절삭) → 박스 → (×12) → EA' ≠ EA
[표준 경로] 대체단위 → (×UMREZ/UMREN, 분수 연산) → 기준단위 → 최종 출력 시 1회만 반올림
표준 함수 모듈은 내부적으로 분자·분모를 유지한 채 연산하고, 단위별 소수 자릿수(T006-ANDEC)에 맞춰 마지막에 한 번만 반올림합니다. "중간 반올림 금지, 최종 1회 반올림"이 이 글 전체를 관통하는 원칙입니다.
💻 실전 코드 3단계
1단계 — MARM 계수 조회와 오차 재현 (기본 예제)
먼저 잘못된 방식이 실제로 어떤 오차를 만드는지 눈으로 확인합니다.
REPORT zdemo_uom_step1.
PARAMETERS: p_matnr TYPE matnr DEFAULT 'FG-1001'.
START-OF-SELECTION.
" 자재의 기준단위와 대체단위 환산 계수 조회
SELECT SINGLE meins FROM mara
WHERE matnr = @p_matnr
INTO @DATA(lv_base_uom).
SELECT matnr, meinh, umrez, umren
FROM marm
WHERE matnr = @p_matnr
INTO TABLE @DATA(lt_marm).
LOOP AT lt_marm INTO DATA(ls_marm).
WRITE: / |{ ls_marm-meinh }: { ls_marm-umrez } { lv_base_uom }| &&
| = { ls_marm-umren } { ls_marm-meinh }|.
ENDLOOP.
" 하드코딩 나눗셈의 오차 재현: 1000 EA -> 박스 -> 다시 EA
DATA: lv_qty_ea TYPE menge_d VALUE 1000,
lv_qty_box TYPE menge_d.
lv_qty_box = lv_qty_ea / 12. " 83.333 (소수 3자리 절삭)
DATA(lv_back) = lv_qty_box * 12. " 999.996 -> 오차 발생!
WRITE: / |직접 나눗셈 왕복 결과: { lv_back } (원본 { lv_qty_ea })|.
실행하면 왕복 결과가 999.996으로 출력됩니다. 단 한 번의 왕복에서 이미 정합성이 깨진 것입니다.
2단계 — 표준 변환 함수 적용과 에러 처리 (실무 시나리오)
출하 지시 라인의 EA 수량을 박스 단위로 환산해 인터페이스로 내보내는 상황을 가정합니다. 자재별 환산은 MD_CONVERT_MATERIAL_UNIT을 사용하고, 예외를 로깅합니다.
DATA: lv_qty_out TYPE menge_d,
lt_log TYPE STANDARD TABLE OF string.
LOOP AT lt_delivery_items ASSIGNING FIELD-SYMBOL().
CALL FUNCTION 'MD_CONVERT_MATERIAL_UNIT'
EXPORTING
i_matnr = -matnr
i_in_me = -meins " 예: EA
i_out_me = 'KAR' " 목표: 박스
i_menge = -lfimg
IMPORTING
e_menge = lv_qty_out
EXCEPTIONS
error_in_application = 1
error = 2
OTHERS = 3.
IF sy-subrc <> 0.
" MARM에 대체단위 미등록 등 — 라인을 버리지 말고 로깅 후 원단위 유지
APPEND |{ -matnr }: KAR 환산 실패 (subrc { sy-subrc })|
TO lt_log.
-qty_conv = -lfimg.
-uom_conv = -meins.
CONTINUE.
ENDIF.
-qty_conv = lv_qty_out.
-uom_conv = 'KAR'.
ENDLOOP.
KG↔G처럼 자재와 무관한 동일 차원 단위 환산에는 T006 기반의 UNIT_CONVERSION_SIMPLE이 적합합니다.
CALL FUNCTION 'UNIT_CONVERSION_SIMPLE'
EXPORTING
input = lv_weight_g " 예: 2500 G
unit_in = 'G'
unit_out = 'KG'
IMPORTING
output = lv_weight_kg " 2.5 KG
EXCEPTIONS
conversion_not_found = 1
division_by_zero = 2
input_invalid = 3
output_invalid = 4
overflow = 5
units_missing = 6
OTHERS = 7.
IF sy-subrc <> 0.
MESSAGE e001(zmm) WITH 'G' 'KG'.
ENDIF.
구분 기준: 박스·팔레트처럼 자재마다 비율이 다른 환산은 MD_CONVERT_MATERIAL_UNIT(MARM 기반), 미터법 등 자재와 무관한 물리 단위 환산은 UNIT_CONVERSION_SIMPLE(T006 기반)입니다. 둘을 혼동하면 conversion_not_found 예외를 만나게 됩니다.
3단계 — 대량 처리용 캐싱 래퍼와 단위 테스트 (프로덕션)
수만 라인을 처리할 때 라인마다 함수 모듈을 호출하면 MARM 반복 조회로 성능이 저하됩니다. 계수를 한 번만 읽어 메모리에 캐싱하고, 분수 연산으로 직접 환산하되 최종 1회만 반올림하는 클래스를 만듭니다.
CLASS zcl_uom_converter DEFINITION FINAL.
PUBLIC SECTION.
METHODS constructor IMPORTING it_matnr TYPE matnr_tab.
METHODS to_base
IMPORTING iv_matnr TYPE matnr
iv_qty TYPE menge_d
iv_uom TYPE meins
RETURNING VALUE(rv_base) TYPE menge_d
RAISING zcx_uom_not_found.
PRIVATE SECTION.
TYPES: BEGIN OF ty_factor,
matnr TYPE matnr,
meinh TYPE meinh,
umrez TYPE umrez,
umren TYPE umren,
END OF ty_factor.
DATA mt_factor TYPE HASHED TABLE OF ty_factor
WITH UNIQUE KEY matnr meinh.
ENDCLASS.
CLASS zcl_uom_converter IMPLEMENTATION.
METHOD constructor.
" 대상 자재의 계수를 1회 벌크 조회 (FOR ALL ENTRIES 대신 IN 활용)
IF it_matnr IS NOT INITIAL.
SELECT matnr, meinh, umrez, umren FROM marm
FOR ALL ENTRIES IN @it_matnr
WHERE matnr = @it_matnr-table_line
INTO TABLE @mt_factor.
ENDIF.
ENDMETHOD.
METHOD to_base.
READ TABLE mt_factor INTO DATA(ls_f)
WITH TABLE KEY matnr = iv_matnr meinh = iv_uom.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_uom_not_found.
ENDIF.
" 분자 곱을 먼저, 나눗셈은 마지막에 — 중간 절삭 방지
rv_base = ( iv_qty * ls_f-umrez ) / ls_f-umren.
ENDMETHOD.
ENDCLASS.
핵심은 ( iv_qty * ls_f-umrez ) / ls_f-umren처럼 곱셈을 먼저 수행해 나눗셈 절삭을 마지막 한 번으로 미루는 것입니다. 합산이 필요한 리포트라면 "기준단위로 전부 합산 후 마지막에 대체단위로 1회 환산"하는 순서를 지킵니다. ABAP Unit 테스트로 왕복 정합성을 검증해 두면 계수 변경 시 회귀를 조기에 잡을 수 있습니다.
METHOD test_round_trip.
" 1000 EA -> 박스 -> EA 왕복 시 원본과 일치해야 함
DATA(lo_conv) = NEW zcl_uom_converter( VALUE #( ( 'FG-1001' ) ) ).
DATA(lv_base) = lo_conv->to_base( iv_matnr = 'FG-1001'
iv_qty = 1000
iv_uom = 'EA' ).
cl_abap_unit_assert=>assert_equals( act = lv_base exp = 1000 ).
ENDMETHOD.
보안·정합성 관점에서는 사용자 입력 단위를 그대로 신뢰하지 말고 CUNIT 변환 엑싯(내부/외부 단위 포맷, 예: 화면의 "ST"와 내부 "ST")을 거치는 것이 일반적으로 안전하며, MARM에 없는 단위는 예외로 명확히 거부해야 합니다.
⚠️ 흔한 실수와 트러블슈팅
Q1. MD_CONVERT_MATERIAL_UNIT이 error_in_application(subrc 1)을 반환합니다.
대부분 MARM에 해당 대체단위가 등록되지 않은 경우입니다. SE16N으로 MARM을 확인하고, MM02 추가 데이터의 단위 탭에서 환산 계수를 등록하세요. 자재 무관 단위(KG↔G)를 이 함수로 환산하려 한 경우에도 실패할 수 있으니 UNIT_CONVERSION_SIMPLE로 전환합니다.
Q2. 표준 함수를 썼는데도 리포트 합계가 미세하게 어긋납니다.
라인별로 대체단위 환산 후 합산했을 가능성이 큽니다. 반올림은 라인 수만큼 누적됩니다. 기준단위로 전부 합산한 뒤 총계에 대해 1회만 환산하도록 순서를 바꾸세요. 또한 T006-ANDEC(단위별 소수 자릿수)이 0인 단위(예: EA)는 정수로 반올림되므로, 소수 수량이 필요한 단위인지도 점검해야 합니다.
Q3. 화면에서 입력받은 단위로 조회하면 MARM에서 못 찾습니다.
단위는 내부 포맷(MSEHI)과 외부 포맷(MSEH3/MSEH6)이 다릅니다. 예를 들어 화면의 "PC"는 내부적으로 "ST"일 수 있습니다. CONVERSION_EXIT_CUNIT_INPUT으로 내부 포맷으로 변환한 뒤 MARM을 조회하세요.
Q4. S/4HANA에서도 MARM을 그대로 써도 되나요?
S/4HANA 온프레미스 2023 기준으로 MARM은 유효한 테이블입니다. 다만 신규 개발에서는 릴리스된 CDS 뷰 I_ProductUnitOfMeasure를 통한 조회가 일반적으로 권장되며, 클라우드 ABAP 환경에서는 고전 함수 모듈 대신 릴리스된 API 사용이 필요합니다.
🚀 이 다음에 살펴볼 주제
기준단위 환산을 익혔다면, 같은 원리가 적용되는 통화 환산(TCURR, CONVERT_TO_LOCAL_CURRENCY)과 비교해 보면 SAP의 "기축 거쳐 환산" 철학이 더 선명해집니다. 이어서 판매단위(MVKE)·발주단위(MARC/EINA)처럼 프로세스별 기본 단위가 결정되는 로직, CDS 뷰에서 unit_conversion 내장 함수로 환산을 DB 레이어로 내리는 방법, 그리고 RAP 기반 앱에서 수량 필드에 @Semantics.quantity.unitOfMeasure 어노테이션을 붙여 UI 반올림을 제어하는 주제로 확장해 보시길 권합니다.
📚 함께 보면 좋은 문서
- SAP Help Portal — SAP S/4HANA 온프레미스 제품 문서 (Logistics, Material Master 영역)
- SAP Help — ABAP Keyword Documentation 7.58 (QUAN/UNIT 타입, 산술 연산 규칙)
- SAP Help — ABAP Platform 문서 (CDS unit_conversion 함수, 릴리스된 API)
- SAP Community — 단위 환산 함수 모듈 활용 사례 및 Q&A
- SAP Business Accelerator Hub — I_ProductUnitOfMeasure CDS 뷰 명세
- SAP Help — SAP ERP 문서 (ECC 자재마스터 단위 관리)
댓글 0
아직 댓글이 없습니다.