DEV ℧ Developer Diary

[EffectiveJava] item60 - 정확한 답이 필요하다면 float와 double은 피하라

float와 double 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계싼하도록 세심하게 설계되었다.

정확한 결과가 필요할 때는 사용하면 안 된다. float와 double 타입은 특히 금융 관련 계산과는 맞지 않는다. 0.1 혹은 10의 음의 거듭 제곱 수(10⁻¹, 10⁻² 등)를 표현할 수 없기 때문이다.

float와 double의 금융 계산

예시 1

예를 들어 주머니에 1.03 달러가 있었는데 그중 42센트를 썻다고 해보자.

System.out.println(1.03 - 0.42);

안타깝게도 이 코드는 0.6100000000000001을 출력한다. 이는 특수한 사례도 아니다.

예시 2

이번엔 주머니에 1달러가 있는데 10센트 짜리 사탕 9개를 샀다고 해보자.

System.out.println(1.00 - 9 * 0.10);

이 코드는 0.09999999999999998을 출력한다.

결과값을 출력하기도 전에 반올림하면 해결되리라 생각할지 모르지만, 반올림을 해도 틀린 답이 나올 수 있다.

예시 3

예를 들어 주머니에는 1달러가 있고, 선반에는 10센트, 20센트, 30센트, … 1달러 짜리의 맛있는 사탕이 놓여 있다고 해보자.

10센트짜리부터 하나씩, 살 수 있을 때까지 사보자. 사탕을 몇개나 살 수 있고, 잔돈을 얼마가 남을까?

오류 발생! 금융 계산에 부동소수 타입을 사용했다.

public static void main(String[] args1) {
    double funds = 1.00;
    int itemBought = 0;
    for (double price = 0.10; funds >= price; price += 0.10) {
        funds -= price;
        itemBought++;
    }
    System.out.println(itemsBought + "개 구입");
    System.out.println("잔돈(달러):" + funds);
}

프로그램을 실행해보면 사탕 3개를 구입한 후 잔돈은 0.3999999999999999달러가 남았음을 알게된다. 물론 잘못된 결과다!

BigDecimal, int, long의 금융 계산

금융 계산에는 BigDecimal, int, long을 사용해야 한다.

다음은 앞서의 코드에서 double 타입을 BigDecimal로 교체만 했다. BigDecimal의 생성자 중 문자열을 받는 생성자를 사용했음에 주목하자. 계산시 부정확한 값이 사용되는 걸 막기 위한 필요한 조치다.

BIgDecimal을 사용한 해법. 속도가 느리고 쓰기 불편하다.

public static void main(String[] args) {
    final BigDecimal TEN_CENTS = new BigDecimal("0.10");

    int itemsBought = 0;
    BigDecimal funds = new BigDecimal("1.00");
    for (BigDecimal price = TEN_CENTS;
            funds.compareTo(price) >= 0;
            price = price.add(TEN_CENTS)) {
        funds = funds.subtract(price);
        itemsBought++;
    }
    System.out.println(itemsBought + "개 구입");
    System.out.println("잔돈(달러): " + funds);
}

이 프로그램을 실행하면 사탕 4개를 구입한 후 잔돈은 0달러가 남는다. 드디어 올바른 답이 나왔다.

BigDecimal의 단점

하지만 BigDecimal에서는 단점이 두가지가 있다.

  • 기본 타입보다 쓰기가 훨씬 불편하다.
  • 기본 타입보다 성능이 훨씬 느리다.

단발성 계산이라면 느리다는 문제는 무시할 수 있지만, 쓰기 불편하다는 점은 못내 아쉬울 것이다.

int와 long의 사용

BigDecimal의 대안으로 int와 long 타입을 쓸 수 있지만, 다룰 수 있는 값의 크기가 제하되고, 소수점을 직접 관리해야한다.

이번 예에서는 모든 계산을 달러 대신 센트로 수행하면 이 문제가 해결된다. 다음은 이 방식으로 구현해본 코드이다.

정수 타입을 사용한 해법

public static void main(String[] args) {
    int itemsBought = 0;
    int funds = 100;
    for (int price = 10; funds >= price; price += 10) {
        funds -= price;
        itemsBought++;
    }
    System.out.println(itemsBought + "개 구입");
    System.out.println("잔돈(센트): " + funds);
}

BigDecimal? int? long?

소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경 쓰지 않겠다면 BigDecimal을 사용하자.

BigDecimal이 제공하는 여덟 가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있다. 법으로 정해진 반올림을 수행하는 비즈니스 계산에서 아주 편리한 기능이다.

반면, 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 int나 long을 사용하면 된다.

숫자를 아홉 자리 십진수로 표현할 수 있다면 int를 사용하고, 열여덟 자리 시진수로 표현할 수 있다면 long을 사용하자. 열려덟 자리를 넘어가면 BigDecimal을 사용해야 한다.