블랙잭 미션을 수행하면서 이런 피드백을 받았었습니다.
돈과 관련된 건 BigDecimal을 사용해 보면 좋을 것 같습니다!
왜 int나 double로 연산하면 안 되는 걸까요?
직접 두 눈으로 확인해 보겠습니다.
double vs BigDecimal
간단한 테스트를 먼저 해보겠습니다.
잔돈을 구하는 간단한 로직입니다.
!!!
테스트는 실패합니다.
눈앞에서 소중한 `0.00000000000000002`가 사라졌습니다.
BigDecimal으로 해보면 어떨까요?
소중한 `0.00000000000000002`를 되찾았어요!
double이 정확한 소수점을 표현하지 못한 이유는 부동소수점이기 때문입니다.
즉, 정확한 값이 아닌 근사치 값으로 표현되고 있어 계산에 오차가 생겼던 것입니다.
BigDecimal
BigDecimal은 소수점의 정확한 연산을 가능하게 해 줍니다.
공식 문서에 따르면 BigDecimal은
Immutable, arbitrary-precision signed decimal numbers.
변하지 않는(Immutable), 정밀도에 제한이 없는(arbitrary-precision), 부호 있는 십진수(signed decimal numbers)로 설명하고 있습니다.
생성
BigDecimal는 생성자 , 정적 팩토리 메서드 두 가지 모두 지원합니다.
생성자
생성자로 테스트해보겠습니다.
`1000.1` 기대한 값과 다른 값이 생성되었네요!
사실 똑똑한 인텔리제이는 힌트를 주고 있었습니다.
예측하지 못한 생성자 호출이라며, `new BigDecimal(”1000.1”)`로 바꾸라고 제안해주고 있네요!
정적 팩토리 메서드
`valueOf()` 메서드를 통해서도 생성할 수 있습니다.
이번에는 `double`에 대한 테스트도 통과했네요!
여기서 흥미로운 부분이 있습니다.
🤔 생성자로 쓰면 되지 왜 굳이 정적 팩토리 메서드를 만들었을까?
그 이유는 캐싱에 있습니다.
앞서, BigDecimal가 Immutable 이라고 했던 설명 기억나시나요?
불변 객체이므로 값이 바뀔 때마다 항상 새로운 객체를 생성합니다.
메모리를 효율적으로 관리하는 것이 중요하고 이를 위해 캐싱을 사용합니다.
메서드 내부를 보면,
`0 ~ 10`까지 값들은 `ZERO_THROUGH_TEN` 에서 캐싱하고 있습니다.
또한 `0`, `1`, `2`, `-1`, `10`은 상수로 선언되어 있습니다.
`double`을 인자로 갖는 메서드에서도 `0.0` 대신 상수 `ZERO` 사용을 제안하고 있습니다.
비교 연산
compareTo() vs equals()
`1000.10`과 `1000.1`을 비교해 보겠습니다.
`compareTo`는 테스트가 성공했는데, `equals`는 실패했네요!
`equals` 메서드 내부를 보면,
`scale` 값이 다르면 `false`를 반환하고 있습니다.
`1000.10` scale은 2이고,
`1000.1` scale 값은 1이므로
`equals` 비교에서는 `false`가 반환된 것입니다.
🤔 그럼 동등성 비교에서는 항상 equals을 쓰는 게 더 낫지 않을까요?
결론부터 말하자면, 그렇지 않습니다.
`compareTo` 메서드를 살펴보면,
scale이 같은 경우에 빠른 비교 연산이 가능하다고 명시돼 있습니다.
이 밖에도 다양한 연산과 반올림 정책들이 있습니다.
기본적인 연산
x.add(y); // 더하기
x.subtract(y); // 빼기
x.multiply(y); // 곱하기
x.divide(y, RoundingMode.HALF_EVEN); // 나누기
x.remainder(y); // 나머지
반올림 정책
RoundingMode.UP // 양수 올림, 음수 내림
RoundingMode.DOWN // 양수 내림, 음수 올림
RoundingMode.CEILING // 올림
RoundingMode.FLOOR // 내림
RoundingMode.HALF_UP // 5 이상 올림, 5 미만 내림
RoundingMode.HALF_DOWN // 6 이상 올림, 6 미만 내림
RoundingMode.HALF_EVEN // 5이면 앞 자리에 따라 올림(짝수-내림/홀수-올림), 5 초과 올림, 5 미만 내림
RoundingMode.UNNECESSARY // 소수면 ArithmeticException
정리
- 돈, 소수점을 다루는 연산은 BigDecimal을 쓰자 ⭐️
- 생성
- 생성자(new) - 부동소수점 표현(float, double) 값은 사용하지 말자
- 정적 팩토리 메서드(valueOf) - 부동소수점 표현(float, double) 값을 사용 시에 쓰자, 캐싱 시 유리
- 비교 연산
- compareTo - scale 값 비교가 불필요한 상황에 쓰자. 대부분의 상황이 그렇다.
- equals - scale까지 고려해야 하는 상황에 제한적으로 쓰자
참고
'Java' 카테고리의 다른 글
상태를 공유하는 시나리오 테스트 DynamicTest (25) | 2024.04.01 |
---|---|
의존성 역전을 언제 쓰나요? (3) | 2024.03.25 |
객체 생성 방식에 기준이 있나요? (28) | 2024.03.11 |
도메인에 Record를 써도 될까요? (7) | 2024.03.04 |