돈은 float에 담지 않는다 — 최소단위 정수 저장
2026-07-24 · 2 min read
$ python -c 'print(0.1 + 0.2)'
0.30000000000000004
3줄 요약
float/double은 2진 부동소수라 10진 소수를 정확히 못 담는다- 금액은 최소단위 정수(원, 센트, satoshi)로 저장한다
- 저장 타입보다 어디서 반올림하는지가 더 자주 사고를 낸다
왜 안 되나
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False한 건이면 무시할 오차다. 문제는 정산이 누적이라는 것이다. 10만 건 합산하면 오차가 눈에 보이는 자리로 올라온다. 그리고 금융 도메인에서 "1원 안 맞음"은 버그가 아니라 사고로 취급된다.
최소단위 정수로 저장
통화마다 소수점 자릿수(minor unit)가 다르다.
| 통화 | 소수 자릿수 | 저장값 예시 |
|---|---|---|
| KRW 원 | 0 | 1500 → 1500 |
| USD 달러 | 2 | $15.00 → 1500 (센트) |
| JPY 엔 | 0 | ¥1500 → 1500 |
| BTC | 8 | 0.0015 BTC → 150000 (satoshi) |
컬럼은 BIGINT. 그리고 금액 컬럼 옆에는 통화 컬럼이 붙어야 한다.
amount = 1500 만 보면 1500원인지 $15.00인지 알 수 없다.
amount_minor BIGINT NOT NULL,
currency CHAR(3) NOT NULL -- ISO 4217원화만 쓸 거라도 통화 컬럼은 넣어둔다. 나중에 붙이면 기존 로우를 전부 백필해야 한다. 컬럼 하나가 마이그레이션 하나보다 싸다.
DECIMAL은 언제 쓰나
DB에 DECIMAL(18, 2) 같은 10진 타입이 있으면 그것도 정답이다.
연산이 10진으로 정확히 돌아간다. 정수 방식보다 읽기가 편하다.
문제는 애플리케이션 언어다. DB에서 DECIMAL로 꺼내도
드라이버가 float로 변환해버리면 그 순간 오차가 들어온다.
Python이면 decimal.Decimal, Java면 BigDecimal로 받는지 확인해야 한다.
# psycopg는 numeric을 Decimal로 준다. 여기서 float()로 캐스팅하면 도루묵
total = sum(row["amount"] for row in rows) # Decimal 유지
total = sum(float(row["amount"]) for row in rows) # 여기서 깨짐정수 방식을 더 선호하는 이유가 이거다. 언어·드라이버·JSON 직렬화를
다 통과해도 정수는 정수로 남는다. JSON에는 Decimal 타입이 없다.
진짜 사고는 반올림에서 난다
타입을 맞춰도 나눗셈이 들어오면 끝이 안 맞는다. 1000원을 3명에게 나누면 333.33...원이다.
1000 / 3 → 333, 333, 333 → 합계 999. 1원이 증발했다.
규칙을 정해야 한다. 나는 잔여분을 마지막(또는 첫) 대상에게 몰아주는 방식을 쓴다.
def split(total: int, n: int) -> list[int]:
base, rest = divmod(total, n)
return [base + (1 if i < rest else 0) for i in range(n)]
assert split(1000, 3) == [334, 333, 333]
assert sum(split(1000, 3)) == 1000
assert sum(split(-1000, 3)) == -1000 # 음수도 합이 보존되는지핵심은 어떤 규칙이냐가 아니라 합계가 항상 보존되는지다. 그리고 그 규칙은 코드가 아니라 문서에 적혀 있어야 한다. CS팀이 "왜 저 사람만 1원 더 받았냐"는 문의에 답할 수 있어야 하니까.
한 줄 요약
최소단위
BIGINT+ 통화 컬럼으로 저장하고, 나눗셈에는 합계 보존 테스트를 붙인다.