티스토리 뷰

배경

정산 관련 업무를 맡았을 때, 업체 및 운영 유저가 발주를 위해 사용하던 서비스였고, 그 과정에서 발주 금액에 대한 세금을 계산하고 세금계산서를 발행해주는 로직이 있었습니다.

이펙티브 자바에서 나오는 "정확한 답이 필요하다면 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)에 대한 더 자세한 내용은 회사 동료가 추천해주신 아래 링크를 참고하는걸 추천드립니다.

 

Floating Point 부동소수점

 

johngrib.github.io

내가 경험한 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
링크
«   2024/11   »
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
글 보관함