[Java] 돈 계산에 double 쓰면 안 되는 이유: BigDecimal 사용법과 함정
![[Java] 돈 계산에 double 쓰면 안 되는 이유: BigDecimal 사용법과 함정](https://blog.kakaocdn.net/dna/cYK7Jt/dJMcabkcXCL/AAAAAAAAAAAAAAAAAAAAAKtTfCDl5-dODoBhffDN-SQPDv6THIOwHeIKc4Haa_e-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1788188399&allow_ip=&allow_referer=&signature=cZVOKXDBoseQRTVh%2BUlKpq6c1wo%3D)
[Java] 돈 계산에 double 쓰면 안 되는 이유: BigDecimal 사용법과 함정
0.1 + 0.2 == 0.3이 false라는 건 다들 한 번쯤 들었는데, 이게 정산 금액 1원 차이로 돌아오면 더 이상 퀴즈가 아니에요. 돈을 다루는 백엔드에서 부동소수점은 "가끔 틀리는" 게 아니라 구조적으로 10진 소수를 표현 못 하는 타입이고, 그래서 BigDecimal이 규율이 돼요. 그런데 BigDecimal에도 함정이 세 개 있어요.
이 글은 double이 왜 돈에 안 되는지(원리), BigDecimal의 3대 규율(생성자·비교·나눗셈), 그리고 "경로 전체"를 지키는 타입 설계(DB·JSON까지) 순서로 짚어볼게요. 함정 시리즈 1편이에요.
01. double은 0.1을 저장하지 못해요

System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(1.03 - 0.42); // 0.6100000000000001
System.out.println(10000.0 * 0.07); // 700.0000000000001 (수수료 7%)
System.out.println(0.1 + 0.2 == 0.3); // false
버그가 아니라 IEEE 754 부동소수점의 본질이에요. double은 값을 2진수로 저장하는데, 10진수 0.1은 2진수로는 무한소수예요(1/3이 10진수로 0.333...인 것처럼요). 그래서 저장하는 순간부터 이미 근사값이고, 연산할수록 오차가 누적돼요.
"오차가 0.00000000000000004인데 무슨 문제야"라고 생각하기 쉬운데, 돈에서는 두 경로로 사고가 돼요. 첫째, 반올림 경계 — 699.9999...원을 반올림하면 700원이지만, 700.0000...1원에 버림 정책이 걸리면 옆 시스템과 1원이 어긋나요. 둘째, 대량 집계 — 건당 티끌이 수백만 건 합산에서 원 단위 차이로 자라요. 정산 대사에서 "1원 안 맞음"으로 발견되는 그 종류예요. 돈은 단 1원도 "대충"이 안 되는 도메인이라, 근사 타입 자체가 실격이에요.
02. BigDecimal 규율 ①, 생성은 문자열로
그래서 자바의 답은 BigDecimal(10진수를 정확히 표현)인데, 첫 줄부터 함정이 있어요.
new BigDecimal(0.1);
// → 0.1000000000000000055511151231257827021181583404541015625
double 생성자는 이미 오염된 double 값을 그대로 받아요. 0.1의 근사값이 정밀하게 박제되는 거죠. BigDecimal을 썼는데도 오차가 나는 미스터리가 대개 여기서 시작돼요.
new BigDecimal("0.1"); // ✅ 문자열 생성자 — 정확히 0.1
BigDecimal.valueOf(0.1); // ✅ 내부적으로 문자열 경유 — 안전

03. 규율 ②, 비교는 compareTo로
new BigDecimal("1.0").equals(new BigDecimal("1.00")); // false!
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")); // 0 (같음)
equals는 값뿐 아니라 scale(소수점 자릿수)까지 비교해요. 1.0과 1.00이 다른 객체라는 거죠. "금액이 같은데 if문이 안 탄다"의 단골 원인이고, HashSet·HashMap의 키로 쓸 때도 같은 함정이 있어요. 값 비교는 항상 compareTo() == 0이에요.
04. 규율 ③, 나눗셈엔 scale과 반올림을 명시
BigDecimal.ONE.divide(new BigDecimal("3"));
// → ArithmeticException: Non-terminating decimal expansion
BigDecimal.ONE.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
// → 0.33 ✅ 자릿수와 반올림 방식 명시
1/3 같은 무한소수는 "어디서 끊을지"를 모르면 예외예요. 나눗셈(그리고 모든 반올림 지점)에는 scale과 RoundingMode를 명시해요.
여기서 중요한 게 — 반올림 정책은 기술 선택이 아니라 비즈니스 결정이에요. 수수료를 올림할지 내림할지, 분배 후 남는 1원을 누가 갖는지는 코드가 아니라 정책이 정해요. 참고로 HALF_UP(사사오입)이 일상적 반올림이고, HALF_EVEN(은행가 반올림)은 .5를 짝수 쪽으로 보내 대량 집계의 편향을 없애는 방식이에요. 뭐가 됐든 한 곳(공통 유틸·정책 문서)에 명시하고 전사가 통일하는 게 핵심이에요 — 시스템마다 반올림이 다르면 그게 바로 대사 불일치예요.
05. 한 구간만 double이어도 전체가 오염돼요

API는 BigDecimal인데 중간 계산 한 줄이 doubleValue()를 거치면, 거기서 오염되고 끝까지 전파돼요. 경로 전체의 타입 규율이 필요해요.
- 자바 — BigDecimal로 통일. 대안으로 최소 단위 정수(원 단위면 long, 소수점 거래면 "전"·"마이크로원" 단위 정수)도 강력해요 — 정수 연산은 오차 자체가 없으니까요. 오차는 없지만 범위는 별개라, 곱셈 중간값이 커지는 계산은 오버플로를 따로 챙겨야 해요. 암호화폐·외환처럼 소수점 깊은 도메인에서 흔한 선택이에요.
- DB —
DECIMAL(19, 4)같은 고정소수점 컬럼. FLOAT·DOUBLE 컬럼은 금액에 금지예요. JPA는 BigDecimal ↔ DECIMAL이 자연 매핑돼요. - JSON — 함정이 하나 더 있어요. 자바스크립트의 Number도 double이에요. 금액을 JSON 숫자로 내리면 프론트에서 같은 오차가 나요(큰 ID가 깨지는 것과 같은 원리). 금액은 문자열("12345.67")이나 최소 단위 정수로 내리는 게 안전해요.
06. 자주 만나는 문제
BigDecimal을 쓰는데도 값이 미세하게 어긋나요
어딘가에서 double 생성자나 doubleValue() 경유를 의심해요. 경로 전체를 추적하면 한 줄이 나와요.
금액 비교 if문이 가끔 안 타요
equals의 scale 비교예요. compareTo() == 0으로 바꿔요.
옆 시스템과 정산이 1원 안 맞아요
반올림 정책(방식·적용 시점·자릿수)이 서로 다른 거예요. 어느 단계에서 몇 자리로 어떻게 반올림하는지를 양쪽이 문서로 맞춰요. 건별 반올림 vs 합산 후 반올림의 차이도 단골 원인이에요.
프론트에서 금액이 이상하게 보여요
JSON 숫자로 내린 금액이 JS double에서 깨진 거예요. 문자열로 내려요.
정리
돈 계산은 결국 다섯 가지 습관으로 압축돼요. 한 곳의 구멍이 경로 전체를 오염시키는 종류라, 컨벤션·공통 유틸로 박아두는 게 답이에요.
- double은 쓰지 않아요 — 2진수는 10진 소수를 못 담아요.
- BigDecimal 생성은 문자열로 — double 생성자는 오염값을 박제해요.
- 값 비교는 compareTo — equals는 scale까지 봐요.
- 나눗셈·반올림엔 scale과 RoundingMode 명시 — 반올림 정책은 비즈니스 결정이에요.
- DB DECIMAL·JSON 문자열까지 경로 전체 통일 — 한 구간만 double이어도 끝까지 번져요.
이어지는 함정은 자정에 터지는 버그예요. 타임존과 날짜 처리로 넘어가요.
출처: Java — BigDecimal · What Every Computer Scientist Should Know About Floating-Point Arithmetic
'백엔드 > 프레임워크 & 언어' 카테고리의 다른 글
| [Java] 타임존 버그 정리: LocalDateTime vs Instant, UTC 저장, 날짜 경계 함정 (0) | 2026.08.14 |
|---|---|
| [Spring] 애너테이션이 안 먹을 때 트러블슈팅: 트랜잭션, 캐시, 비동기 증상별 진단 가이드 (0) | 2026.08.05 |
| [Spring] 순환 참조 해결과 빈 초기화 함정: 생성자 주입, @Lazy, @PostConstruct 주의점 (1) | 2026.08.04 |
| [Spring] @Async 함정 정리: 스레드풀 설정, 예외 증발, 트랜잭션·컨텍스트 미전파 (0) | 2026.08.03 |
| [Spring] @TransactionalEventListener 정리: AFTER_COMMIT, 이벤트와 트랜잭션 타이밍 (0) | 2026.07.31 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."