// 10,000원 50전을 표현하기 위해 정수를 사용할 경우
int amount = 1000050; // 10,000원 50전 (원 단위)
double price = 10.25;
double quantity = 3;
double total = price * quantity; // 기대값: 30.75
System.out.println(total); // 출력: 30.750000000000004 (오차 발생)
double은 뭐가 문제인걸까?
double은 내부적으로 수를 저장할 때 이진수의 근사치를 저장한다.
저장된 수를 다시 십진수로 표현하면서 정확도 문제가 발생한다.
BigDecimal 타입은 내부적으로 수를 십진수로 저장하여 작은 수와 큰 수의 연산에 대해 거의 무한한 정밀도를 보장한다.
precision
unscale
의 동의어precision
은 11이 아닌 9이다.scale
fraction
과 동의어DECIMAL128
flot, double과 달리 초기화가 어려워 자주 쓰는 0, 1, 10은 쓰기 편하게 미리 상수로 정의되어 있다.
// 0
BigDecimal.ZERO
// 1
BigDecimal.ONE
// 10
BigDecimal.TEN
double 타입으로 부터 BigDecimal 타입을 초기화하는 가장 안전한 방법은 문자열의 형태로 생성자에 전달하여 초기화하는 것이다.
double 타입의 값을 그대로 전달할 경우 앞서 사칙연산 결과에서 본 것과 같이 이진수의 근사치를 가지게 되어 예상과 다른 값을 얻을 수 있다.
// double 타입을 그대로 초기화하면 기대값과 ㅏ른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);
// 문자열로 초기화하면 정상 인식
0.01
new BigDecimal("0.01");
// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);
BigDecimal은 기본 타입이 아닌 오브젝트이기 때문에 특히, 동등 비교 연산을 유의해야 한다. double
타입을 사용하던 습관대로 무의식적으로 ==
기호를 사용하면 예기치 않은 연산 결과를 초래할 수 있다.
BigDecimal a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.010");
// 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
따라서, BigDecimal
값을 비교하려면 아래와 같은 방법을 사용해야 한다.
equals()
메서드 사용두 BigDecimal
객체의 값과 스케일이 정확히 같은지 확인한다.
스케일이 다르면 값이 같더라도 false
를 반환한다.
BigDeciaml a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.010");
boolean isEqual = a.equals(b); // false (스케일이 다름)
compareTo()
메서드 사용숫자 값만 비교하며 스케일은 무시한다.
0
: 값이 같음1
: 첫 번째 값이 더 큼-1
: 두 번째 값이 더 큼BigDecimal a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.010");
int result = a.compareTo(b); // 0 (숫자 값은 같음)
⚠️ 주의사항
- 숫자 값만 비교하고 싶다면
compareTo()
를 사용하는 것이 일반적이다.equlas()
는 스케일까지 엄격히 비교하므로 주의가 필요하다.==
연산자는 절대 사용하지 말아야 한다.
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// 더하기
// 13
a.add(b);
// 빼기
// 7
a.sbtract(b);
// 곱하기
// 30
a.mutlply(b);
// 나누기
// 3.333333...
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
a.divide(b);
// 나누기
// 3.333
a.divide(b, 3, RoundingMode.HALF_EVEN);
// 나누기 후 나머지
// 전체 자리수를 34개로 제한
// 1
a.remainder(b, MathContext.DECIMAL128);
// 절대값
// 3
new BigDecimal("-3").abs();
// 두 수 중 최소값
// 3
a.min(b);
// 두 수 중 최대값
// 10
a.max(b);
// POJO 목록에서 BigDecimal 타입을 가진 특정 필드의 합을 반환
BigDecimal sumOfFoo = fooList.stream()
.map(FooEntity::getFooBigDecimal)
.filter(foo -> Objects.nonNull(foo))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 특정 BigDecimal 필드를 기준으로 오름차순 정렬된 리스트를 반환
foolist.stream()
.sorted(Comparator.comparing(it -> it.getAmount()))
.collect(Collectors.toList());
// 위와 동일한 기능, 정렬된 새로운 리스트를 반환하지 않고 원본 리스트를 바로 정렬
foolist.sort(Comparator.comparing(it -> it.getAmount()));
foo DECIMAL(5,2) DEFAULT 0.00 NOT NULL
DECIMAL
타입은 ResultSet
인터페이스의 getBigDecimal()
, getString()
2개 메서드로 획득이 가능하다. JPA 또한 별도의 작업 없이 엔티티 필드에 BigDecimal 타입을 사용하여 처리하면 된다.