TECH/웹개발

[Java] "돈 계산할 때 double 쓰면 망합니다" — BigDecimal의 필요성과 사용법

런코리치 2026. 3. 11. 08:00

자바 개발자로서 시스템을 구축하다 보면 숫자를 다룰 일이 정말 많습니다. 특히 금융, 결제, 이커머스 도메인에서 '금액'은 시스템의 신뢰도와 직결되는 핵심 데이터입니다. 하지만 많은 초보 개발자가 실수하는 부분이 소수점 계산을 위해 습관적으로 double이나 float 타입을 사용하는 것입니다. 전 아직 금융, 결제, 이커머스 관련 개발을 해본적은 없으나 추후 해당 분야에서 개발하고 싶은 욕구가 있어 관련 내용을 알아보고자 합니다.

오늘은 왜 자바의 기본 실수 타입이 금융 데이터 처리에 부적합한지, 그리고 이를 완벽하게 해결해주는 java.math.BigDecimal을 실무에서 어떻게 제대로 사용하는지 상세히 파헤쳐 보겠습니다.


1. 부동 소수점(Floating Point)의 배신: 0.1 + 0.2 ≠ 0.3

우리는 수학적으로 $0.1 + 0.2$가 당연히 $0.3$이라고 믿습니다. 하지만 자바에서 아래 코드를 실행하면 충격적인 결과를 마주하게 됩니다.

Java
 
double a = 0.1;
double b = 0.2;
System.out.println(a + b); 
// 출력 결과: 0.30000000000000004

$0.00000000000000004$라는 미세한 오차가 발생했습니다. "고작 이 정도 차이가 무슨 대수냐"라고 할 수 있지만, 이 연산이 수백만 명의 사용자가 사용하는 결제 시스템에서 하루에도 수억 번씩 일어난다고 가정해 봅시다. 누적된 오차는 곧 금전적 사고로 이어지고, 이는 개발자의 책임이 됩니다.

2. 왜 오차가 발생하는가? (이진수의 한계)

컴퓨터는 내부적으로 모든 데이터를 0과 1인 이진수(Binary)로 처리합니다. 문제는 십진수 소수점 중 일부가 이진수로 변환될 때 무한 순환 소수가 된다는 점입니다.

예를 들어, 십진수 $0.1$을 이진수로 바꾸면 $0.0001100110011...$로 끝없이 반복됩니다. 컴퓨터 메모리는 한정되어 있기 때문에 특정 지점에서 이 숫자를 강제로 끊어버리고 근삿값을 저장합니다. 이를 부동 소수점(Floating Point) 방식이라고 하며, 이 과정에서 발생하는 미세한 값의 손실(Rounding Error)이 연산 시 누적되는 것입니다.


3. 구원투수: BigDecimal 클래스

자바는 이러한 정밀도 문제를 해결하기 위해 java.math.BigDecimal 클래스를 제공합니다. BigDecimal은 숫자를 이진수가 아닌 십진수 형태 그대로 저장하며, 가변 길이를 지원하여 정밀도에 제한이 없습니다.

✅ 반드시 기억해야 할 생성 규칙 (Anti-Pattern 주의)

BigDecimal을 쓸 때 가장 흔히 하는 실수가 또 다시 double을 인자로 넣는 것입니다.

Java
 
// ❌ 절대 금지: double의 오차까지 함께 저장됩니다.
BigDecimal wrong = new BigDecimal(0.1); 
// 결과: 0.10000000000000000555111512312578...

// ✅ 권장 방법 1: String을 사용하세요.
BigDecimal correct1 = new BigDecimal("0.1");

// ✅ 권장 방법 2: 정적 팩토리 메서드를 사용하세요.
BigDecimal correct2 = BigDecimal.valueOf(0.1); 
// 내부적으로 Double.toString()을 호출하여 정확한 값을 가져옵니다.

4. BigDecimal 실전 사칙연산 및 반올림 정책

BigDecimal은 객체이므로 기본 연산자(+, -, *, /)를 사용할 수 없으며 전용 메서드를 사용해야 합니다.

사칙연산 예제

Java
 
BigDecimal x = new BigDecimal("1000.50");
BigDecimal y = new BigDecimal("200.25");

BigDecimal sum = x.add(y);         // 더하기: 1200.75
BigDecimal diff = x.subtract(y);    // 빼기: 800.25
BigDecimal product = x.multiply(y); // 곱하기: 200350.125

나눗셈의 핵심: RoundingMode

나눗셈(divide)을 할 때는 주의가 필요합니다. $10 / 3$처럼 무한 소수가 발생하는 경우, 반올림 정책을 명시하지 않으면 ArithmeticException이 발생하며 프로그램이 뻗어버립니다.

Java
 
BigDecimal val = new BigDecimal("10");
// ❌ 예외 발생 가능성 있음
// val.divide(new BigDecimal("3")); 

// ✅ 소수점 셋째 자리까지 구하고, 반올림(HALF_UP) 수행
BigDecimal result = val.divide(new BigDecimal("3"), 3, RoundingMode.HALF_UP);
// 결과: 3.333

실무에서 자주 쓰는 RoundingMode:

  • HALF_UP: 우리가 아는 일반적인 반올림 (5 이상 올림).
  • FLOOR: 내림.
  • CEILING: 올림.

5. 비교 연산의 함정: equals() vs compareTo()

BigDecimal에서 값의 크기를 비교할 때 equals()를 쓰면 낭패를 봅니다. equals()는 값과 소수점 자리수(scale)가 모두 같아야 true를 반환하기 때문입니다.

Java
 
BigDecimal val1 = new BigDecimal("1.0");
BigDecimal val2 = new BigDecimal("1.00");

System.out.println(val1.equals(val2));    // false (자리수가 다름)
System.out.println(val1.compareTo(val2)); // 0 (값만 비교, 0이면 같음)

따라서 크기 비교는 무조건 compareTo()를 사용하는 것이 금융권 프로젝트의 표준입니다. val1.compareTo(val2) > 0 이면 val1이 큰 것입니다.


6. 성능과 메모리: 무조건 BigDecimal이 답일까?

BigDecimal은 만능처럼 보이지만 대가가 따릅니다.

  1. 성능 저하: 기본형(double, long) 연산보다 속도가 훨씬 느립니다. 수조 번의 복잡한 물리 시뮬레이션에서는 double이 나을 수 있습니다.
  2. 메모리 사용량: 객체이기 때문에 기본형보다 메모리를 더 많이 차지합니다.

하지만 금융 데이터라면 성능보다 '정확성'이 1순위입니다. 현대의 서버 성능에서 결제 로직에 BigDecimal을 쓴다고 해서 성능 병목이 생기는 일은 거의 없습니다.


🏁 결론: "돈은 정직해야 합니다"

우리가 개발하는 시스템에서 숫자는 단순한 데이터가 아닙니다. 누군가의 자산이고, 신뢰의 지표입니다. 32세 개발자로서 수많은 코드를 짜오며 느낀 점은, "나중에 고쳐야지" 하는 안일함이 가장 무섭다는 것입니다.

삼성전자나 SK하이닉스 주가처럼 소수점이 중요한 데이터를 다루거나, ISA 계좌의 수익률을 계산하는 모듈을 만든다면 고민하지 말고 BigDecimal을 선택하세요.

요약하자면:

  1. 돈 계산에는 절대 double을 쓰지 않는다.
  2. BigDecimal 생성 시에는 반드시 String을 넣거나 valueOf()를 쓴다.
  3. 비교는 compareTo()로 한다.
  4. 나눗셈 시에는 RoundingMode를 꼭 명시한다.

정확한 코드가 정확한 서비스를 만듭니다. 오늘도 'Run Code' 하시고, 그 결과로 'Rich' 해지시길 바랍니다!