티스토리 뷰
배경
정산 관련 업무를 맡았을 때, 업체 및 운영 유저가 발주를 위해 사용하던 서비스였고, 그 과정에서 발주 금액에 대한 세금을 계산하고 세금계산서를 발행해주는 로직이 있었습니다.
이펙티브 자바에서 나오는 "정확한 답이 필요하다면 float와 double은 피하라" 내용에 따라서 BigDecimal로 처리하려고 했습니다.
public class SettlementCalculator {
private SettlementCalculator() {}
public static Integer calculateTotalSupplyPrice(final int price, final TaxationType taxationType) {
if(taxationType != (TaxationType.TAX)) {
return price;
}
BigDecimal bdPrice = new BigDecimal(price);
BigDecimal rate = new BigDecimal(1.1);
return bdPrice.divide(rate, 0, RoundingMode.FLOOR).intValue();
}
}
업체마다 과세와 비과세로 나뉘어져 있었는데, 과세일 경우 공급가액(price)에서 1.1을 나눈 값을 제공해주는 메서드입니다.
이번 포스팅에서는 위 메서드로 인해 어떤 문제가 있었는지 이야기해보면서 BigDecimal에 대해서 정리해보겠습니다.
BigDecimal이란?
먼저 BigDecimal이 무엇인지 알아봅시다.
BigDecimal은 Java 언어에서 숫자를 정밀하게 저장하고 표현할 수 있는 유일한 방법입니다.
소수점을 저장할 수 있는 가장 크기가 큰 타입인 double은 소수점의 정밀도에 있어 한계가 있어서 값이 유실될 수 있습니다.
Java 언어에서 돈과 소수점을 다룬다면 BigDecimal은 선택이 아니라 필수인 셈이죠.
BigDecimal의 유일한 단점은 느린 속도와 기본 타입보다 조금 불편한 사용법 뿐입니다.
소수점의 정밀도 한계?
소수점의 정밀도에 한계가 있다는 표현이 잘 안 와닿아서 테스트를 해봤습니다.
1.03 - 0.42의 값을 예상해봅시다.
당연하게도 0.61로 예상해볼 수 있습니다.
하지만 자바 환경에서 테스트해보면 다음과 같은 결과가 나오게 됩니다.
public class Test {
public static void main(String[] args) {
double a = 1.03;
double b = 0.42;
System.out.println(a - b);
}
}
부동소수점(float, double)에 대한 더 자세한 내용은 회사 동료가 추천해주신 아래 링크를 참고하는걸 추천드립니다.
내가 경험한 BigDecimal 문제
부동소수점의 문제와 BigDecimal을 사용해야 하는 이유는 이쯤하면 될 것 같고, 제가 어떤 문제를 겪었는지 서술하겠습니다.
위 메서드에서는 간단한 금액에 대해서는 잘 동작하였으나 문제는 복잡한 금액을 계산할 때 발생했습니다.
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Test {
public static void main(String[] args) {
int simplePrice = 20000;
Integer rowPriceResult = calculateTotalSupplyPrice(simplePrice);
System.out.println("간단한 금액 : " + rowPriceResult);
int complicatedPrice = 234131414;
Integer highPriceResult = calculateTotalSupplyPrice(complicatedPrice);
System.out.println("복잡한 금액 : " + highPriceResult);
}
public static Integer calculateTotalSupplyPrice(final int price) {
BigDecimal bdPrice = new BigDecimal(price);
BigDecimal rate = new BigDecimal(1.1);
return bdPrice.divide(rate, 0, RoundingMode.FLOOR).intValue();
}
}
복잡한 금액의 결과값은 계산기를 통해서 확인해보면 다음과 같습니다.
하지만, 위 코드의 결과는 달랐습니다.
왜 이런 문제가 발생할까요?
BigDecimal 생성자
결론은 BigDecimal 생성자 파라미터로 double 타입 변수를 사용했던 게 문제였습니다.
아래 이미지는 BigDecimal 생성자에 대한 설명입니다.
간략하게 해석해보자면 다음과 같습니다.
이 생성자의 결과는 다소 예측 불가능할 수 있습니다.
생성자에게 전달되는 값 또한 부동소수점이기 때문입니다.
정확한 값을 사용하려면 파라미터를 문자열(문자열 생성자)로 넘기거나, valueOf 정적 메서드를 사용하라고 합니다.
문자열 생성자를 사용했을 때
public static Integer calculateTotalSupplyPrice(final int price) {
BigDecimal bdPrice = new BigDecimal(price);
BigDecimal rate = new BigDecimal("1.1");
return bdPrice.divide(rate, 0, RoundingMode.FLOOR).intValue();
}
문자열로 파라미터를 넘겼을 때 다음과 같은 결과를 얻을 수 있었습니다.
valueOf를 사용했을 때
public static Integer calculateTotalSupplyPrice(final int price) {
BigDecimal bdPrice = new BigDecimal(price);
BigDecimal rate = BigDecimal.valueOf(1.1);
return bdPrice.divide(rate, 0, RoundingMode.FLOOR).intValue();
}
valueOf 메서드를 사용했을 때 다음과 같은 결과를 얻을 수 있었습니다.
결론
해당 문제를 겪으면서 다음과 같은 생각을 했습니다.
- 큰 금액에 대한 테스트 케이스를 고려했다면
- 문서를 좀 더 꼼꼼하게 읽어봤더라면
좀 더 꼼꼼하게 개발하는 습관을 들여야겠습니다.
읽어주셔서 감사합니다~!
'개발 노트' 카테고리의 다른 글
- Total
- Today
- Yesterday
- 클린 아키텍처
- BOJ
- node.js
- 테라폼
- 디자인패턴
- C++
- 이펙티브 자바
- Spring
- 정규표현식
- 백준
- AWS
- 객체지향
- 이팩티브 자바
- 클린 코드
- Java
- BAEKJOON
- MSA
- Olympiad
- Kotlin
- kkoon9
- Algorithm
- kotest
- 프로그래머스
- programmers
- JPA
- Spring Boot
- 디자인 패턴
- Effective Java
- 알고리즘
- 코테
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |