Java에서 BigDecimal 마스터하기: 부동소수점 오류 없이 정확한 금액 계산

目次

1. 소개

Java에서 수치 계산의 정밀도 문제

Java 프로그래밍에서는 수치 계산이 일상적으로 수행됩니다. 예를 들어, 제품 가격을 계산하거나 세금·이자를 산정하는 등 많은 애플리케이션에서 이러한 연산이 필요합니다. 그러나 floatdouble과 같은 부동소수점 타입을 사용해 계산하면 예상치 못한 오류가 발생할 수 있습니다.

이는 floatdouble이 값을 이진 근사값으로 표현하기 때문입니다. 십진법으로 정확히 표현할 수 있는 “0.1”이나 “0.2”와 같은 값도 이진법으로는 정확히 나타낼 수 없으며, 그 결과 작은 오류가 누적됩니다.

금융 또는 정밀 계산에 BigDecimal이 필수인 이유

이러한 오류는 금융 계산 및 정밀 과학·공학 계산과 같은 분야에서 치명적일 수 있습니다. 예를 들어 청구서 계산에서 1엔 차이만 발생해도 신뢰도 문제가 생길 수 있습니다.

바로 여기서 Java의 BigDecimal 클래스가 빛을 발합니다. BigDecimal임의 정밀도의 십진수를 처리할 수 있으며, float·double 대신 사용하면 오류 없이 수치 계산을 수행할 수 있습니다.

이 글을 통해 얻을 수 있는 것

본 글에서는 Java에서 BigDecimal을 사용하는 기본 방법, 고급 기법, 그리고 흔히 발생하는 오류와 주의사항을 체계적으로 설명합니다.

Java에서 금액 계산을 정확히 처리하고 싶거나 프로젝트에 BigDecimal 도입을 고려하고 있는 분들에게 유용합니다.

2. BigDecimal이란?

BigDecimal 개요

BigDecimal은 Java에서 고정밀 십진수 연산을 가능하게 하는 클래스입니다. java.math 패키지에 속하며, 금융·회계·세금 계산 등 오차를 용납할 수 없는 연산을 위해 설계되었습니다.

Java의 float·double은 숫자를 이진 근사값으로 저장하므로 “0.1”·“0.2”와 같은 십진수를 정확히 표현하지 못해 오류가 발생합니다. 반면 BigDecimal은 값을 문자열 기반 십진수 표현으로 저장해 반올림·근사 오류를 억제합니다.

임의 정밀도 숫자 다루기

BigDecimal의 가장 큰 특징은 “임의 정밀도”입니다. 정수부와 소수부 모두 이론적으로 사실상 무한한 자리수를 처리할 수 있어 자리수 제한으로 인한 반올림이나 자리 손실이 없습니다.
예를 들어, 다음과 같은 큰 수도 정확히 다룰 수 있습니다:

BigDecimal bigValue = new BigDecimal("12345678901234567890.12345678901234567890");

이처럼 정밀도를 유지하면서 연산을 수행할 수 있는 것이 BigDecimal의 주요 강점입니다.

주요 사용 사례

BigDecimal은 다음과 같은 상황에서 권장됩니다:

  • 금융 계산 — 이자, 세율 계산 등 금융 애플리케이션
  • 청구서/견적 금액 처리
  • 고정밀을 요구하는 과학·공학 계산
  • 장기 누적으로 오류가 쌓이는 프로세스

예를 들어 회계 시스템 및 급여 계산에서 1엔 차이만 발생해도 큰 손실이나 분쟁으로 이어질 수 있기 때문에 BigDecimal의 정밀도가 필수적입니다.

3. BigDecimal 기본 사용법

BigDecimal 인스턴스 생성 방법

일반 숫자 리터럴과 달리 BigDecimal은 보통 문자열로 생성하는 것이 좋습니다. double·float로 만든 값은 이미 이진 근사 오류를 포함하고 있을 수 있기 때문입니다.

권장 (문자열로 생성):

BigDecimal value = new BigDecimal("0.1");

피해야 함 (double로 생성):

BigDecimal value = new BigDecimal(0.1); // may contain error

연산 수행 방법

BigDecimal은 일반 연산자(+, -, *, /)를 사용할 수 없습니다. 대신 전용 메서드를 사용해야 합니다.

덧셈 (add)

BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("2.3");
BigDecimal result = a.add(b); // 12.8

뺄셈 (subtract)

BigDecimal result = a.subtract(b); // 8.2

곱셈 (multiply)

BigDecimal result = a.multiply(b); // 24.15

나눗셈 (divide) 및 반올림 모드

나눗셈은 주의가 필요합니다. 나누어떨어지지 않을 경우, 반올림 모드를 지정하지 않으면 ArithmeticException이 발생합니다.

BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33

여기서는 “소수점 2자리”와 “반올림(반올림 절반 올림)”을 지정합니다.

setScale를 사용한 스케일 및 반올림 모드 설정

setScale는 지정된 자리수로 반올림하는 데 사용할 수 있습니다.

BigDecimal value = new Big BigDecimal("123.456789");
BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // 123.46

일반적인 RoundingMode 값:

Mode NameDescription
HALF_UPRound half up (standard rounding)
HALF_DOWNRound half down
HALF_EVENBanker’s rounding
UPAlways round up
DOWNAlways round down

BigDecimal은 불변(Immutable)이다

BigDecimal불변입니다. 즉, 산술 메서드(add, subtract 등)는 원래 값을 수정하지 않고 새 인스턴스를 반환합니다.

BigDecimal original = new BigDecimal("5.0");
BigDecimal result = original.add(new BigDecimal("1.0"));
System.out.println(original); // still 5.0
System.out.println(result);   // 6.0

4. BigDecimal 고급 사용법

값 비교: compareTo와 equals의 차이

BigDecimal에서는 값을 비교하는 두 가지 방법이 있습니다: compareTo()equals(). 이 두 메서드는 동작이 다릅니다.

  • compareTo()는 숫자값만 비교하고(스케일은 무시합니다).
  • equals()는 스케일(소수점 자리수)까지 포함하여 비교합니다.
    BigDecimal a = new BigDecimal("10.0");
    BigDecimal b = new BigDecimal("10.00");
    
    System.out.println(a.compareTo(b)); // 0 (values are equal)
    System.out.println(a.equals(b));    // false (scale differs)
    

포인트: 금액과 같은 수치적 동등성 검사는 compareTo()를 일반적으로 권장합니다.

문자열과의 변환

사용자 입력 및 외부 파일 가져오기 시 String 타입과의 변환이 흔히 사용됩니다.

String → BigDecimal

BigDecimal value = new Big BigDecimal("1234.56");

BigDecimal → String

String str = value.toString(); // "1234.56"

valueOf 사용

Java에는 BigDecimal.valueOf(double val)도 있지만, 이 역시 내부적으로 double의 오차를 포함하므로 문자열로부터 생성하는 것이 더 안전합니다.

BigDecimal unsafe = BigDecimal.valueOf(0.1); // contains internal error

MathContext를 통한 정밀도 및 반올림 규칙

MathContext는 정밀도와 반올림 모드를 한 번에 제어할 수 있어, 여러 연산에 공통 규칙을 적용할 때 유용합니다.

MathContext mc = new MathContext(4, RoundingMode.HALF_UP);
BigDecimal result = new BigDecimal("123.4567").round(mc); // 123.5

산술 연산에서도 사용할 수 있습니다:

BigDecimal a = new BigDecimal("10.456");
BigDecimal b = new BigDecimal("2.1");
BigDecimal result = a.multiply(b, mc); // 4-digit precision

null 검사 및 안전한 초기화

폼에서 null이나 빈 값을 전달할 수 있으므로, 방어 코드가 일반적입니다.

String input = ""; // empty
BigDecimal value = (input == null || input.isEmpty()) ? BigDecimal.ZERO : new BigDecimal(input);

BigDecimal의 스케일 확인

소수점 자리수를 확인하려면 scale()을 사용합니다:

BigDecimal value = new BigDecimal("123.45");
System.out.println(value.scale()); // 3

5. 일반적인 오류와 해결 방법

ArithmeticException: 무한 소수 확장

오류 예시:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b); // exception

이는 “1 ÷ 3”이며, 무한 소수가 되므로 반올림 모드/스케일을 지정하지 않으면 예외가 발생합니다.

해결 방법: 스케일과 반올림 모드 지정

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // OK (3.33)

double에서 직접 생성할 때 발생하는 오류

double을 직접 전달하면 이미 이진 오류가 포함될 수 있습니다 — 예상치 못한 값을 생성합니다.

나쁜 예시:

BigDecimal val = new BigDecimal(0.1);
System.out.println(val); // 0.100000000000000005551115123...

올바름: 문자열 사용

BigDecimal val = new BigDecimal("0.1"); // exact 0.1

참고: BigDecimal.valueOf(0.1)은 내부적으로 Double.toString()을 사용하므로 new BigDecimal("0.1")과 “거의 동일”합니다 — 하지만 문자열이 100% 가장 안전합니다.

스케일 불일치로 인한 equals 오해

equals()는 스케일을 비교하므로 값이 수치적으로 같아도 false를 반환할 수 있습니다.

BigDecimal a = new BigDecimal("10.0");
BigDecimal b = new BigDecimal("10.00");

System.out.println(a.equals(b)); // false

해결: 수치적 동등성을 위해 compareTo() 사용

System.out.println(a.compareTo(b)); // 0

불충분한 정밀도로 인한 예상치 못한 결과

setScale을 반올림 모드 없이 사용하면 — 예외가 발생할 수 있습니다.

나쁜 예시:

BigDecimal value = new BigDecimal("1.2567");
BigDecimal rounded = value.setScale(2); // exception

해결:

BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // OK

입력 값이 유효하지 않을 때 NumberFormatException

숫자로 파싱할 수 없는 유효하지 않은 텍스트가 전달되면 (예: 사용자 입력 / CSV 필드), NumberFormatException이 발생합니다.

해결: 예외 처리 사용

try {
    BigDecimal value = new BigDecimal(userInput);
} catch (NumberFormatException e) {
    // show error message or fallback logic
}

6. 실전 사용 예시

여기서는 BigDecimal이 실제로 어떻게 사용될 수 있는지 보여주는 실제 시나리오를 소개합니다. 특히 재무/회계/세금 계산에서 정확한 숫자 처리의 중요성이 명확해집니다.

가격 계산에서 소수 처리 (분수 반올림)

예시: 10% 소비세를 포함한 가격 계산

BigDecimal price = new BigDecimal("980"); // price w/o tax
BigDecimal taxRate = new BigDecimal("0.10");
BigDecimal tax = price.multiply(taxRate).setScale(0, RoundingMode.HALF_UP);
BigDecimal total = price.add(tax);

System.out.println("Tax: " + tax);         // Tax: 98
System.out.println("Total: " + total);     // Total: 1078

포인트:

  • 세금 계산 결과는 종종 정수로 처리되며, 반올림을 위해 setScale(0, RoundingMode.HALF_UP)을 사용합니다.
  • double은 오류를 발생시키기 쉽습니다 — BigDecimal을 권장합니다.

할인 계산 (% OFF)

예시: 20% 할인

BigDecimal originalPrice = new BigDecimal("3500");
BigDecimal discountRate = new BigDecimal("0.20");
BigDecimal discount = originalPrice.multiply(discountRate).setScale(0, RoundingMode.HALF_UP);
BigDecimal discountedPrice = originalPrice.subtract(discount);

System.out.println("Discount: " + discount);         // Discount: 700
System.out.println("After discount: " + discountedPrice); // 2800

포인트: 가격 할인 계산은 정밀도를 잃지 않아야 합니다.

단가 × 수량 계산 (전형적인 비즈니스 앱 시나리오)

예시: 298.5 엔 × 7개 품목

BigDecimal unitPrice = new BigDecimal("298.5");
BigDecimal quantity = new BigDecimal("7");
BigDecimal total = unitPrice.multiply(quantity).setScale(2, RoundingMode.HALF_UP);

System.out.println("Total: " + total); // 2089.50

포인트:

  • 분수 곱셈에 대한 반올림 조정.
  • 회계 / 주문 시스템에 중요합니다.

복리 계산 (재무 예시)

예시: 3% 연 이자 × 5년

BigDecimal principal = new BigDecimal("1000000"); // base: 1,000,000
BigDecimal rate = new BigDecimal("0.03");
int years = 5;

BigDecimal finalAmount = principal;
for (int i = 0; i < years; i++) {
    finalAmount = finalAmount.multiply(rate.add(BigDecimal.ONE)).setScale(2, RoundingMode.HALF_UP);
}

System.out.println("After 5 years: " + finalAmount); // approx 1,159,274.41

Point:

  • Repeated calculations accumulate errors — BigDecimal avoids this.

Validation & Conversion of User Input

public static BigDecimal parseAmount(String input) {
    try {
        return new BigDecimal(input).setScale(2, RoundingMode.HALF_UP);
    } catch (NumberFormatException e) {
        return BigDecimal.ZERO; // treat invalid input as 0
    }
}

Points:

  • Safely convert user-provided numeric strings.
  • Validation + error fallback improves robustness.

7. Summary

The Role of BigDecimal

In Java’s numeric processing — especially monetary or precision-required logic — the BigDecimal class is indispensable. Errors inherent in float / double can be dramatically avoided by using BigDecimal.

This article covered fundamentals, arithmetic, comparisons, rounding, error handling, and real-world examples.

Key Review Points

  • BigDecimal handles arbitrary-precision decimal — ideal for money and precision math
  • Initialization should be via string literal , e.g. new BigDecimal("0.1")
  • Use add() , subtract() , multiply() , divide() , and always specify rounding mode when dividing
  • Use compareTo() for equality — understand difference vs equals()
  • setScale() / MathContext let you finely control scale + rounding
  • Real business logic cases include money, tax, quantity × unit price etc.

For Those About to Use BigDecimal

Although “handling numbers in Java” looks simple — precision / rounding / numeric error problems always exist behind it. BigDecimal is a tool that directly addresses those problems — mastering it lets you write more reliable code.

At first you may struggle with rounding modes — but with real project usage, it becomes natural.

Next chapter is an FAQ section summarizing common questions about BigDecimal — useful for review and specific semantic searches.

8. FAQ: Frequently Asked Questions About BigDecimal

Q1. Why should I use BigDecimal instead of float or double?

A1.
Because float/double represent numbers as binary approximations — decimal fractions cannot be represented exactly. This causes results such as “0.1 + 0.2 ≠ 0.3.”
BigDecimal preserves decimal values exactly — ideal for money or precision-critical logic.

Q2. What is the safest way to construct BigDecimal instances?

A2.
Always construct from string.
Bad (error):

new BigDecimal(0.1)

Correct:

new BigDecimal("0.1")

BigDecimal.valueOf(0.1) uses Double.toString() internally, so it’s almost same — but string is the safest.

Q3. Why does divide() throw an exception?

A3.
Because BigDecimal.divide() throws ArithmeticException when result is a non-terminating decimal.
Solution: specify scale + rounding mode

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);

Q4. What’s the difference between compareTo() and equals()?

A4.

  • compareTo() checks numeric equality (scale ignored)
  • equals() checks exact equality including scale
    new BigDecimal("10.0").compareTo(new BigDecimal("10.00")); // → 0
    new BigDecimal("10.0").equals(new BigDecimal("10.00"));    // → false
    

Q5. How do I perform rounding?

A5.
Use setScale() with explicit rounding mode.

BigDecimal value = new BigDecimal("123.4567");
BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // 123.46

Main rounding modes:

  • RoundingMode.HALF_UP (round half up)
  • RoundingMode.DOWN (round down)
  • RoundingMode.UP (round up)

Q6. Can I check decimal digits (scale)?

A6.
Yes — use scale().

BigDecimal val = new BigDecimal("123.45");
System.out.println(val.scale()); // → 3

Q7. How should I handle null/empty input safely?

A7.
Always include null checks + exception handling.

public static BigDecimal parseSafe(String input) {
    if (input == null || input.trim().isEmpty()) return BigDecimal.ZERO;
    try {
        return new BigDecimal(input.trim());
    } catch (NumberFormatException e) {
        return BigDecimal.ZERO;
    }
}