float과 double은 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 '근사치'로 계산하도록 설계되었다.
따라서 정확한 결과가 필요할 땐 사용해선 안되며, 특히 금융 관련 계산과는 맞지 않는다. 10의 음의 거듭제곱 수를 표현할 수 없어서다.
float과 double이 사용하는 IEEE-754 부동소수점 관련 참고하면 좋은 레퍼런스가 두 곳 있어서 참고했다 :) [참고1] / [참고2] / [참고3]
1달러가 있다고 하고, 10, 20, 30, 40... 센트짜리 사탕이 있다면 살 수 있을때까지 순서대로 산다고 생각해보자.
0.1+0.2+0.3+0.4 = 1.0 이라서 아래 코드가 4개가지 구매 가능하고, 잔돈이 없다고 생각할 수 있다.
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
funds -= price;
itemsBought++;
}
// 3
System.out.println(itemsBought + "개 구입");
// 0.3999999999999999
System.out.println("잔돈(달러): " + funds);
}
하지만 3개를 구입하고 잔돈은 0.399... 달러가 남았다고 뜬다. 따라서 0.4달러 사탕을 살 수 없었던 것.
double은 fraction(가수) 부분을 최대 52자리까지 나타낼 수 있고, 52자리까지 표현되지 않으면 round(올림) 처리를 한다.
0.1은 이진수로 나타내면 무한소수이기 떄문에 부동소수점으로 완벽하게 표현할 수 없고, 조금씩의 오차가 쌓여 0.0000...1의 오차를 냈다.
이 문제를 해결하기 위해서는 BigDecimal, int, long을 사용해야한다.
BigDecimal 사용
아래 코드를 사용하면 사탕 4개를 구매 후 잔돈은 0달러라는 올바른 정답이 나온다.
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".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);
}
BigDecimal 생성자에 문자열을 받는 생성자를 사용했다. 이는 계산시 부정확한 값이 사용되는 걸 막기위해 필요한 조치다.
소수점 0.1이 들어가면 유한소수로 완전히 나눠떨어지지 않아서 1.1을 각각 문자열과 double로 파라미터로 넣어보았다.
이진수의 근사치를 통해 계산하는 double로 넘어간 1.1은 부정확한 1.1을 반환함을 알 수 있다.
BigDecimal은 이외에도 숫자를 정밀하게 다루는 많은 기능을 제공하며 여덟가지 반올림 모드를 이용해 완벽히 제어할 수 있다.
하지만 기본 타입보다 쓰기가 불편하고 훨씬 느리다.
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);
}
정리
정확한 답이 필요한 계산에선 float과 double을 피하자.
소수점 추적은 시스템에 맡기고 코딩시 불편함과 성능 저하를 신경쓰지 않겠다면 BigDecimal을 이용하자.
성능이 중요하고 소수점을 직접 추적할 수 있으며, 숫자가 너무 크지 않다면 int(~9자리), long(~18자리)을 사용하자.
* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.
'Reading > Effective Java' 카테고리의 다른 글
[Effective-Java] Item 62, 63.다른 타입이 적절하다면 문자열 사용을 피하라, 문자열 연결은 느리니 주의하라 (0) | 2021.11.26 |
---|---|
[Effective-Java] Item 61.박싱된 기본 타입보다는 기본 타입을 사용하라 (0) | 2021.11.26 |
[Effective-Java] Item 59.라이브러리를 익히고 사용하라 (0) | 2021.11.25 |
[Effective-Java] Item 58.전통적인 for 문보다는 for-each 문을 사용하라 (0) | 2021.11.22 |
[Effective-Java] Item 57.지역변수의 범위를 최소화하라 (0) | 2021.11.14 |