[Java] BigDecimal에 관한 고찰 🕵️‍♀️

✨New Wisdom✨·2021년 3월 1일
14

📕 Java 📕

목록 보기
22/24
post-thumbnail

이번 미션에서 BigDecimal에 대한 언급이 많았는데,
도대체 그게 뭐지 🤷‍♀️ 나만 몰라 ❓
에서부터 출발한 BigDecimal에 대한 고찰 ~

이번주 회고에서 앞으로 포스팅을 내가 이해한 글을 설명하듯이 ! 작성하기로 다짐한
내용을 최대한 반영하려고 노력중 ✍️
누군가 이 글을 읽고 있다면 아 얘가 이런짓을 하고 있구나 가볍게 봐주길 ㅎㅎ 🙈

BigDecimal을 왜 쓰지 ?

Test

과연 위 테스트 케이스는 통과를 할까? 정답은 👀

허허 어이없게도 실패합니다.
왜 실패하냐구요 ?
자바는 IEEE 754 부동 소수점 방식을 사용해서,
정확한 실수를 저장하지 않고 최대한 완벽에 가깝기를 바라는 근사치 값을 저장하기 때문이다 !

지금은 단순히 개발자 하나가 테스트를 위해 작성한 코드이지만,
금융권에서 달러를 다루는 개발자가 double을 이용해서 이런 사소한 값 차이가 발생한다면 🙃

그렇다면 이 문제를 어떻게 해결하느냐 !
우리에겐 부동 소숫점 방식이 아닌, 정수를 이용해 실수를 표현하는 java.math.BigDecimal 클래스가 있다.

Java 언어에서 돈과 소수점을 다룬다면 BigDecimal은 선택이 아니라 필수다.

그렇다면 본격적으로 BigDecimal을 해부해 봅시다.

BigDecimal 클래스

BigDecimal 클래스의 일부를 가져왔다.
여기서 간단하게 몇 가지 변수들의 쓰임 짚고 넘어가겠다.
참고로 BigDecimal은 불변 클래스임

변수

  • intValue : 정수. 정수를 저장하는데 BigInteger를 사용한다.
  • scale : 지수. 정확히는 소수점 첫째 자리부터 0이 아닌 수로 끝나는 위치까지의 총 소수점 자리수이다.
  • precision : 정밀도. 정확히는 0이 아닌 수가 시작하는 위치부터 오른쪽부터 0이 아닌 수로 끝나는 위치까지의 총 자리수이다.
    아래 출력 결과로 각 변수의 의미를 파악하자.

BigDecimal의 생성

BigDecimal의 생성 방법은 여러가지가 있는데,
문자열로 숫자를 표현하는 것이 일반적이다.
기본형 리터럴로는 표현할 수 있는 값의 한계가 있는데, 예를 들어
double 타입의 값을 그대로 전달할 경우
앞서 사칙연산 결과에서 본 것과 같이 예상과 다른 값을 얻을 수도 있다.

BigDecimal val;
val = new BigDecimal("12345.6789");
val = new BigDecimal(12345.678); // 오차가 날 수 있음
val = new BigDecimal(12345);
val = BigDecimal.valueOf(12345.67); // 오차가 날 수 있음
val = BigDecimal.valueOf(123456);

실제로 인텔리제이도 String으로 바꾸라고 알려준다 👀 똑똑이

BigDecimal의 비교

BigDecimal은 Comparable<BigDecimal>를 구현하고 있기 때문에,
equals()를 통해 내용을 비교할 수 있다.
여기서 주의할 점은 12.0112.010은 같지 않다.

BigDecimal의 타입 변환

문자열로 변환

String toPlainString(); // 무조건 다른 기호 없이 숫자로만 표현
String toString(); // 필요시 지수 형태로 표현할 수도 있음

Number를 상속받고 있기 때문에 Number의 기본형으로 변환하는 메서드를 가지고 있다.

int intValue()
long longValue()
float floatValue()
double doubleValue()

또 BigDecimal을 정수형으로 변환하는 메서드 중,
이름 끝에 Exact가 붙은 것은 변환 결과가 변환 타입 범위에 속하지 않으면
ArithmeticException을 발생시킨다.

테스트 성공 !

BigDecimal의 연산

먼저 기본적인 연산을 수행하는 메서드들이다.

BigDecimal add(BigDecimal val)
BigDecimal subtract(BigDecimal val)
BigDecimal multiply(BigDecimal val)
BigDecimal divide(BigDecimal val)
BigDecimal remainder(BigDecimal val)

테스트 성공 !

참고로 BigDecimal은 불변 객체이기 때문에
연산 후 반환 타입이 BigDecimal인 경우 새로운 인스턴스가 반환된다.

추가로 다른 연산과 달리 divide()은 메서드가 다양한 버전으로 오버로딩 되어있다.
어떻게 반올림할지(roundingMode), 몇 번째 자리(scale)에서 반올림 할건지를 지정할 수 있다.

roundingMode

반올림 처리 방법에 대한 것으로 BigDecimal에 정의된 다음 상수들에서 하나를 선택해 사용하면 된다.

  • CEILING – 올림
  • FLOOR – 내림
  • UP – 양수일 경우 올림, 음수일 경우 내림
  • DOWN – 양수일 경우 내림, 음수일 경우 올림
  • HALF_UP – 반올림(5이상 올림, 5미만 버림)
  • HALF_EVEN – 반올림(반올림 자리의 값이 짝수면 HALF_DOWN, 홀수면 HALF_UP)
  • HALF_DOWN – 반올림(6이상 올림, 6미만 버림)
  • UNNECESSARY – 나눗셈 결과가 딱 떨어지지 않으면,ArithmeticException 발생

scale 변경

소수점 위치를 바꾸고 싶을 때는 BigDecimal을 10으로 곱하거나 나누는 것이 아니라,
setScale()을 통해 scale 값을 변경한다.

BigDecimal setScale(int newScale)
BigDecimal setScale(int newScale, int roundingMode)
BigDecimal setScale(int newScale, RounfingMode mode)

주의할 점은 setScale()로 scale을 줄이는 것은 10의 n제곱으로 나누는 것과 같으므로
오차가 발생할 수 있으니 roundingMode를 지정하는 것이 좋다.

생성자 대신 valueOf()를 사용해라 !

이 부분은 개인적인 고찰이니 혹시나 이 글을 누군가 읽고 잘못된 부분이 있다면 반박 부탁드림다 🙈
BigDecimal은 생성자와 정적 팩터리 메서드인 valueOf()를 사용해서 객체를 생성할 수 있는데,
과연 두 개는 어떤 것이 다를까 ❓

고것이 궁금해졌다.

일단 BigDecimal은 Integer클래스처럼 일부 값을 캐싱해 놓고 있다.
왜일까 스스로 답을 해보았는데,

BigDecimal의 주생성자를 가보면,

정말 장황한 생성자를 볼 수 있다.
때문에 BigDecimal이 성능이 느리다고도 하는데,
내 생각에는 이러한 이유 때문에도 캐싱을 해논게 아닌가 싶다.

이쯤에서 정적 팩터리 메서드인valueOf()로 가보면,


정적 팩터리 메서드는 static 변수인 캐싱 값에 바로 접근할 수 있다.
때문에 이미 캐싱되어 있는 값이 존재한다면 생성자를 통하지 않고 그 값을 바로 반환한다.


또 추가로
double실수값을 생성자로 생성해도 연산결과가 정확하지 않을 수 있으니,
valueOf()를 사용해서 생성하는 것이 좋다고도 한다.

오호라 👀
요즘 정적 팩터리 메서드와 캐싱개념들에 대해 살펴봤는데,
BigDecimal을 뜯어보니 굉장히 흥미롭다 👻

profile
🚛 블로그 이사합니다 https://newwisdom.tistory.com/

2개의 댓글

comment-user-thumbnail
2021년 6월 20일

정말 훌륭한 글입니다! BigDecimal에 대해서 자세히 알고 싶었는데, 감사히 배우고 갑니다!👍

답글 달기
comment-user-thumbnail
2023년 10월 19일

좋은 글 잘 읽었습니다 감사합니다!!
잼있네요

답글 달기