파이썬 Decimal 합계가 다른 이유: 행별 반올림과 합산 후 반올림 비교
금액은 문자열에서 Decimal로 만들고, 반올림 단위·방식·시점을 입력 계약으로 고정해야 합계를 재현할 수 있습니다.
적용 환경: Python 3.14 표준 라이브러리. 2026-10-22 확인. Windows Python 3.14.4에서 아래 예제를 임시 작업 폴더에서 실행하고 assert 및 예상 예외 검사를 통과했습니다. 실제 회사 자료·외부 서비스는 사용하지 않았습니다.
Decimal을 쓰면 왜 합계가 자동으로 맞지 않을까요?
이 글은 금전 거래나 세금 계산 규정이 아니라 보고서 숫자를 재현하는 프로그래밍 예제입니다. 입력이 정확해도 각 행을 먼저 반올림한 뒤 더하는 계산과, 원래 값을 모두 더한 뒤 한 번 반올림하는 계산은 다를 수 있습니다. 담당 시스템과 합의한 기준 없이 어느 쪽을 정답으로 바꾸면 안 됩니다.
작은 합성 입력으로 차이를 고정합니다
예제 단위는 소수 둘째 자리까지 표시하는 가상의 금액입니다. 0.005 두 행을 ROUND_HALF_UP으로 각각 처리하면 0.01 두 개라 합계는 0.02입니다. 원래 값 두 개의 합 0.010을 마지막에 처리하면 0.01입니다. 소수 둘째 자리라는 선택과 HALF_UP 방식은 이 예제의 계약일 뿐 모든 통화·정산 규칙의 기본값이 아닙니다.
직접 실행하는 최소 예제
from decimal import Decimal, localcontext, ROUND_HALF_UP
import re
PATTERN = re.compile(r"[0-9]{1,12}(?:\.[0-9]{1,4})?")
CENT = Decimal("0.01")
def calculate_total(values, mode):
if not isinstance(values, list) or not 1 <= len(values) <= 10000:
raise ValueError("1~10000개 문자열의 목록이 필요합니다")
if mode not in {"per_line", "after_sum"}:
raise ValueError("반올림 시점을 명시하세요")
amounts = []
for text in values:
if not isinstance(text, str) or PATTERN.fullmatch(text) is None:
raise ValueError("금액 형식 오류")
amounts.append(Decimal(text))
# 위 입력 크기 제한 안에서 합산할 정밀도를 고정합니다.
with localcontext() as ctx:
ctx.prec = 28
ctx.rounding = ROUND_HALF_UP
if mode == "per_line":
return sum((x.quantize(CENT) for x in amounts), Decimal("0.00"))
return sum(amounts, Decimal("0")).quantize(CENT)
def expect_invalid(values, mode="after_sum"):
try:
calculate_total(values, mode)
except ValueError:
return
raise AssertionError("잘못된 입력이 통과했습니다")
assert calculate_total(["0.005", "0.005"], "per_line") == Decimal("0.02")
assert calculate_total(["0.005", "0.005"], "after_sum") == Decimal("0.01")
assert calculate_total(["2.675"], "after_sum") == Decimal("2.68")
assert calculate_total(["0.1", "0.2"], "after_sum") == Decimal("0.30")
assert calculate_total(["0"], "per_line") == Decimal("0.00")
assert calculate_total(["999999999999.9999"] * 10000, "after_sum") == Decimal("9999999999999999.00")
assert Decimal(0.1) != Decimal("0.1")
for bad in [[], [0.1], ["NaN"], ["Infinity"], ["1e2"], ["-1"],
["1,000"], [" 1"], ["1.23456"], ["1"] * 10001]:
expect_invalid(bad)
expect_invalid(["1"], "unspecified")
print("행별 반올림: 0.02 / 합산 후 반올림: 0.01")
print("2.675 -> 2.68, 0.1 + 0.2 -> 0.30")
print("형식·개수·최대 합계·반올림 시점 검사 통과")
검증한 출력
행별 반올림: 0.02 / 합산 후 반올림: 0.01 2.675 -> 2.68, 0.1 + 0.2 -> 0.30 형식·개수·최대 합계·반올림 시점 검사 통과
문자열 입력과 계산 정밀도는 다른 설정입니다
Decimal(0.1)은 이미 만들어진 이진 부동소수 값을 십진수로 변환합니다. 원래 텍스트를 보존하려면 Decimal("0.1")처럼 시작하세요. 아래 함수는 정수부 1~12자리, 소수부가 있으면 1~4자리인 음수 없는 ASCII 문자열만 받습니다. 최대 10000행과 이 범위에서 덧셈에 충분한 28자리 정밀도를 함께 제한했습니다. ctx.prec는 소수 자릿수가 아니라 유효 숫자 정밀도입니다.
업무 규칙을 코드와 함께 보관하세요
per_line과 after_sum 중 하나를 호출할 때 반드시 전달합니다. 반환값은 Decimal이며 마지막 화면 표시에서만 필요한 문자열 서식을 입히세요. 통화 코드, 최소 단위, 반올림 방식, 행별 처리 여부를 실행 설정·결과와 같이 남기면 시스템 사이의 차이를 비교하기 쉽습니다. 부가 계산에서 나눗셈이나 곱셈을 추가한다면 현재 정밀도 한도를 다시 검증해야 합니다.
거부하는 입력도 문서화해야 합니다
이 함수는 NaN·Infinity·지수 표기·쉼표·앞뒤 공백·float·음수와 빈 목록을 거부합니다. 정규식으로 표기를 제한하므로 특수 Decimal 값이 합계에 섞이지 않습니다. 환불이나 음수 조정이 있는 업무에는 이 규칙을 그대로 쓸 수 없습니다. 공백이나 쉼표를 무조건 제거하면 원본 오류가 숨겨지므로 별도 정제 단계와 변경 기록을 두세요.
보고서 대조와 보안 주의점
최소 재현 자료에는 합성 금액만 넣고 실제 거래처·계좌·결제 내역을 붙이지 마세요. 차이가 나면 먼저 입력 행 수와 중복 키, 통화, 반올림 시점을 구분해서 비교합니다. 계산 결과가 다르다고 실데이터를 자동 보정하거나 지급 파일을 덮어쓰는 작업은 이 예제에 포함하지 않았습니다.
자주 묻는 질문
원화는 0.01로 반올림하면 되나요?
이 글의 0.01은 가상 입력 계약입니다. 실제 업무의 최소 단위와 처리 방식을 확인하고 quantize 인자와 테스트를 바꿔야 합니다.
Decimal이면 무제한 정밀도로 계산되나요?
아닙니다. 연산에는 context 정밀도가 적용됩니다. 입력 범위와 연산 종류를 넓히면 정밀도와 예외 정책도 다시 정해야 합니다.
왜 기본 round()만 쓰지 않나요?
이 예제는 ROUND_HALF_UP과 시점을 코드에 명시해 결과를 재현하려는 목적입니다. 내장 round와 같다고 가정하지 말고 합의한 규칙을 테스트하세요.
공식 문서와 확인일
다음 Python 공식 문서를 2026-10-22에 직접 확인하고 예제와 대조했습니다. 합성 입력의 숫자와 결과는 위 코드를 실행해 재현한 값입니다.
관련 가이드와 다음 검사
작성 안내: AI 보조로 작성했으며 합성 자료로 로컬 코드의 결과를 검증했습니다. 운영자의 실제 경험이나 사람 전문가 검수를 주장하지 않습니다. 입력 계약과 환경이 달라지면 업무용 자료에 적용하기 전에 별도 테스트를 추가하세요.
댓글
댓글 쓰기