-
Java - 돈 계산에 BigDecimal을 써야 하는 이유언어/Java 2026. 6. 29. 11:58
프로젝트를 진행하면서 돈과 관련된 필드는 BigDecimal을 사용했었는데 ,
이 글에서는 double로 돈계산을 할 때 왜 위험한지, BigDecimal이 정확히 어떤 문제를 해결해주는지, 그리고 BigDecimal을 사용할때 마주칠 수 있는 주의점을 코드로 직접 확인하며 정리해보려고 합니다.
1. double의 함정: 2진법은 10진 소수를 정확히 표현하지 못한다
double a = 0.1; double b = 0.2; System.out.println(a + b); // 결과: 0.30000000000000004double은 IEEE 754 이진 부동소수점 방식으로 숫자를 저장합니다. 컴퓨터는 모든 실수를 2의 거듭제곱의 합으로 표현하는데, 0.1처럼 흔한 10진 소수도 2진법으로는 무한소수가 되어버립니다. 10진법에서 1/3이 0.333...으로 끝나지 않는 것과 같은 원리입니다. 결국 컴퓨터는 이 무한소수를 어느 지점에서 끊어서 근사값으로 저장하고, 그 결과 아주 작은 오차가 생깁니다.
이 오차는 한 번이면 무시할 수 있을 정도로 작지만, 연산이 누적되면 커집니다.
double sum = 0; for (int i = 0; i < 10; i++) { sum += 0.1; } System.out.println(sum); // 결과: 0.9999999999999999금액을 더하고 곱하고 나누는 연산이 반복되는 결제·정산 시스템에서, 이런 오차는 "장부가 1원 안 맞는" 형태의 실제 버그로 이어질 수 있습니다.
2. BigDecimal을 쓴다고 끝이 아니다
여기서 주의할 점은 BigDecimal을 쓰는 것 자체가 중요한 게 아니라, 어떻게 생성하느냐가 중요합니다.
BigDecimal wrong = new BigDecimal(0.1); System.out.println(wrong); // 결과: 0.1000000000000000055511151231257827021181583404541015625 BigDecimal right1 = new BigDecimal("0.1"); BigDecimal right2 = BigDecimal.valueOf(0.1); System.out.println(right1); // 0.1 System.out.println(right2); // 0.1new BigDecimal(double) 생성자에 double 리터럴을 그대로 넘기면, 이미 부동소수점으로 깨져버린 값을 그대로 가져와서 BigDecimal로 감싸는 것뿐입니다. BigDecimal은 오차를 자동으로 없애주는 객체가 아니라, 10진수를 정확하게 표현할 수 있는 자료구조일 뿐입니다. 이미 깨진 값을 넣으면 깨진 값 그대로 보존됩니다.
그래서 사용규칙은 명확합니다.
돈 관련 값은 항상 String으로 받아 new BigDecimal("19.99")로 생성하거나, double에서 변환해야 한다면 BigDecimal.valueOf(double)을 사용해야 합니다. new BigDecimal(double)은 절대 쓰지 않아야 합니다.
3. BigDecimal이 정확한 이유: 내부 구조 자체가 다르다
double이 2의 거듭제곱 분수로 값을 표현하는 반면, BigDecimal은 내부적으로 BigInteger unscaledValue와 int scale 두 값으로 숫자를 표현합니다. 예를 들어 19.99는 unscaledValue = 1999, scale = 2로 저장되어 1999 × 10⁻²를 의미합니다.
10진수 기반이기 때문에 10진 소수를 손실 없이 정확히 표현할 수 있어서, 2진법으로 변환하는 과정 자체가 없으니, 애초에 오차가 생길 여지가 없습니다.
BigDecimal p = new BigDecimal("19.99"); BigDecimal q = new BigDecimal("3"); System.out.println(p.multiply(q)); // 결과: 59.974. equals() 주의 : 값이 같아도 false가 나올 수 있다
BigDecimal x = new BigDecimal("1.0"); BigDecimal y = new BigDecimal("1.00"); System.out.println(x.equals(y)); // false System.out.println(x.compareTo(y) == 0); // trueBigDecimal.equals()는 값뿐 아니라 scale(소수점 자릿수)까지 같아야 true를 반환합니다. 1.0과 1.00은 수학적으로 같은 값이지만, equals() 기준으로는 다르다고 판단합니다.
5. 나눗셈은 반올림 정책을 강제로 명시해야 한다
BigDecimal total = new BigDecimal("100"); BigDecimal parts = new BigDecimal("3"); total.divide(parts); // ArithmeticException: Non-terminating decimal expansion; // no exact representable decimal result.100을 3으로 나누면 33.333...처럼 무한소수가 됩니다. BigDecimal은 이를 정확히 표현할 수 없으면 예외를 던져서, 반올림 방식을 직접 선택하도록 강제합니다.
total.divide(parts, 2, RoundingMode.HALF_UP); // 33.33이 부분이 double과 가장 본질적으로 다른 지점입니다. double은 그냥 알아서 (잘린) 근사값을 내놓고 끝나지만, BigDecimal은 어떤 방식으로 반올림할지를 코드에서 명시적으로 선택하게 만듭니다.
돈 계산에서 이게 중요한 이유는, 반올림 방식이 세금 계산·포인트 적립·환율 변환처럼 법적·회계적으로 정해져 있는 경우가 많기 때문입니다. 같은 코드베이스 안에서도 계산 종류마다 다른 반올림 규칙이 필요할 수 있는데, 이를 암묵적으로 처리하면 나중에 정산 불일치가 생겼을 때 어디서 얼마가 어떻게 반올림됐는지 추적이 거의 불가능해집니다.
정리
BigDecimal을 쓰는 이유는 크게 두 가지
- 부동소수점 오차 방지 : double은 2진법 한계로 10진 소수를 정확히 표현하지 못하지만, BigDecimal은 10진수 기반이라 정확
- 반올림 정책의 명시적 통제 : 나눗셈처럼 무한소수가 나오는 연산에서 반올림 방식을 코드 차원에서 강제로 선택하게 만들어, 암묵적 반올림으로 인한 회계 불일치 방지
'언어 > Java' 카테고리의 다른 글
Java - JVM의 작동원리와 스레드 (1) 2025.02.09 Java - 클래스와 인터페이스의 상속 (0) 2023.07.27