개념
- BigDecimal은 Java 언어에서 숫자를 정밀하게 저장하고 표현할 수 있는 유일한 방법이다.
- 소수점을 저장할 수 있는 가장 크기가 큰 타입인 double은 소수점의 정밀도에 있어 한계가 있어 값이 유실될 수 있다.
- Java 언어에서 돈과 소수점을 다룬다면 BigDecimal은 선택이 아니라 필수이다.
- BigDecimal의 유일한 단점은 느린 속도와 기본 타입보다 조금 불편한 사용법 뿐이다.
double 문제점
소수점 이하의 수를 다룰 때 double 타입은 사칙연산시 아래와 같이 우리가 기대한 값과 다른 값을 출력한다. 이유는 double 타입이 내부적으로 수를 저장할 때 이진수의 근사치를 저장하기 때문이다. 저장된 수를 다시 십진수로 표현하면서 아래와 같은 문제가 발생한다. 아래 설명할 BigDecimal 타입은 내부적으로 수를 십진수로 저장하여 아주 작은 수와 큰 수의 연산에 대해 거의 무한한 정밀도를 보장한다.
double a = 10.0000;
double b = 3.0000;
// 기대값: 13
// 실제값: 13.000001999999999
a + b;
// 기대값: 7
// 실제값: 6.999999999999999
a - b;
// 기대값: 30
// 실제값: 30.000013000000997
a * b;
// 기대값: 3.33333...
// 실제값: 3.333332555555814
a / b;
기본 용어
- precision: 숫자를 구성하는 전체 자리수라고 생각하면 편하나, 정확하게 풀이하면 왼쪽부터 0이 아닌 수가 시작하는 위치부터 오른쪽부터 0이 아닌 수로 끝나는 위치까지의 총 자리수이다. unscale과 동의어이다. (ex: 012345.67890의 precision은 11이 아닌 9이다.)
- scale: 전체 소수점 자리수라고 생각하면 편하나, 정확하게 풀이하면 소수점 첫째 자리부터 오른쪽부터 0이 아닌 수로 끝나는 위치까지의 총 소수점 자리수이다. fraction과 동의어이다. (ex: 012345.67890의 scale은 4이다. 하지만 0.00, 0.0의 scale은 모두 1이다.) BigDecimal은 32bit의 소수점 크기를 가진다.
- DECIMAL128: IEEE 754-2008에 의해 표준화된, 부호와 소수점을 수용하며, 최대 34자리까지 표현 가능한 10진수를 저장할 수 있는 형식이다. 2018년 미국 정부의 총 부채액이 15조 7천 500억 달러로 총 14자리 임을 감안하면, 금융권에서 처리되는 대부분의 금액을 수용할 수 있는 크기이다. Java에서는 BigDecimal 타입을 통해 공식적으로 지원한다.
기본 상수
float double 타입과 다르게 BigDecimal 타입은 초기화가 장황한 편이다, 그래서 자주 쓰는 0, 1, 100은 쓰기 편하게 상수로 정의 되어있다.
// 흔히 쓰이는 값은 상수로 정의
// 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");
// 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
// false
a == b;
// 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
// false
a.equals(b);
// 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
// 0
a.compareTo(b);
사칙 연산
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// 더하기
// 13
a.add(b);
// 빼기
// 7
a.subtract(b);
// 곱하기
// 30
a.multiply(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);
소수점 처리
RoundingMode.HALF_EVEN은 Java의 기본 반올림 정책으로 금융권에서 사용하는 Bankers Rounding와 동일한 알고리즘이다. 금융권에서는 시스템 개발시 혼란을 막기 위해 요구사항에 반올림 정책을 명확히 명시하여 개발한다.
// 소수점 이하를 절사한다.
// 1
new BigDecimal("1.1234567890").setScale(0, RoundingMode.FLOOR);
// 소수점 이하를 절사하고 1을 증가시킨다.
// 2
new BigDecimal("1.1234567890").setScale(0, RoundingMode.CEILING);
// 음수에서는 소수점 이하만 절사한다.
// -1
new BigDecimal("-1.1234567890").setScale(0, RoundingMode.CEILING);
// 소수점 자리수에서 오른쪽의 0 부분을 제거한 값을 반환한다.
// 0.9999
new BigDecimal("0.99990").stripTrailingZeros();
// 소수점 자리수를 재정의한다.
// 원래 소수점 자리수보다 작은 자리수의 소수점을 설정하면 예외가 발생한다.
// java.lang.ArithmeticException: Rounding necessary
new BigDecimal("0.1234").setScale(3);
// 반올림 정책을 명시하면 예외가 발생하지 않는다.
// 0.123
new BigDecimal("0.1234").setScale(3, RoundingMode.HALF_EVEN);
// 소수점을 남기지 않고 반올림한다.
// 0
new BigDecimal("0.1234").setScale(0, RoundingMode.HALF_EVEN);
// 1
new BigDecimal("0.9876").setScale(0, RoundingMode.HALF_EVEN);
나누기 처리
BigDecimal b10 = new BigDecimal("10");
BigDecimal b3 = new BigDecimal("3");
// 나누기 결과가 무한으로 떨어지면 예외 발생
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
b10.divide(b3);
// 반올림 정책을 명시하면 예외가 발생하지 않음
// 3
b10.divide(b3, RoundingMode.HALF_EVEN);
// 반올림 자리값을 명시
// 3.333333
b10.divide(b3, 6, RoundingMode.HALF_EVEN);
// 3.333333333
b10.divide(b3, 9, RoundingMode.HALF_EVEN);
// 전체 자리수를 7개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333
b10.divide(b3, MathContext.DECIMAL32);
// 전체 자리수를 16개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333
b10.divide(b3, MathContext.DECIMAL64);
// 전체 자리수를 34개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333333333333333333333
b10.divide(b3, MathContext.DECIMAL128);
// 전체 자리수를 제한하지 않는다.
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. 예외가 발생한다.
b10.divide(b3, MathContext.UNLIMITED);
문자열 변환 출력
NumberFormat format = NumberFormat.getInstance();
format.setMaximumFractionDigits(6);
format.setRoundingMode(RoundingMode.HALF_EVEN);
// 0.123457
format.format(new BigDecimal("0.1234567890"));
유닛 테스트
BigDecimal a = BigDecimal.valueOf(0.1);
BigDecimal b = BigDecimal.valueOf(0.10);
BigDecimal c = BigDecimal.valueOf(0.101);
BigDecimal d = BigDecimal.valueOf(0.001);
// equals()와 동일하기 때문에 소수점 마지막 0까지 동일해야 true
// false
assertThat(a).isEqualTo(b));
// compareTo()와 동일하기 때문에 소수점 마지막 0이 달라도 true
// true
assertThat(a).isEqualByComparingTo(b);
// 두 수가 주어진 오차 범위를 만족하면 true
// true
assertThat(a).isCloseTo(c, within(d));
통화
아래는 build.gradle에 해당 라이브러리의 종속성을 추가하는 방법이다.
compile group: 'org.javamoney', name: 'moneta', version: '1.3', ext: 'pom' // JavaMoney
compile group: 'org.javamoney.lib', name: 'javamoney-lib', version: '1.0', ext: 'pom' // JavaMoney
compile group: 'org.javamoney.lib', name: 'javamoney-calc', version: '1.0' // JavaMoney Caculations
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java] Comparable과 Comparator : 비교를 위한 인터페이스 (0) | 2024.10.22 |
---|---|
[Java] Collections 클래스 (0) | 2024.10.22 |
[Java] Generic 제네릭 (0) | 2024.10.22 |
[Java] long float의 값 뒤에 L, F을 붙여야 하는 이유 (0) | 2024.09.04 |
[Java] 자료형 정리 (0) | 2024.09.04 |