📌 실수 표기법
💡 float와 double 타입은 특히 금융 관련 계산과는 맞지 않는다.
알고리즘 공부를 할 때 해당 내용에 대해 다룬 포스팅이 있다.
- float과 double 타입은 과학과 공학 계산용으로 설계되었다.
- 이진 부동(浮動)소수점 연산에 사용되지만, 데이터라는 한계로 인해 유한한 수만 다룰 수 있어 '근사치'를 계산한다.
- 정확한 결과가 필요할 때는 사용하지 마라.
📌 어설프게 작성된 코드들
1️⃣ 기본적인 실수 연산
System.out.println(1.03 - 0.42); // 0.6100000000000001
System.out.println(1.00 - 9 * 0.10); // 0.09999999999999998
- 0.1 혹은 10의 음의 거듭 제곱 수를 표현할 수 없다.
- 결괏값을 출력하기 전에 반올림을 하더라도 틀린 답이 나올 수 있다.
2️⃣ 금융 계산
public class Main {
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++;
}
System.out.println(itemsBought + "개 구입");
System.out.println("잔돈(달러): " + funds); // 잔돈(달러): 0.3999999999999999
}
}
- 4개를 구입하고 잔돈이 0원이 남아야 하지만, 결과는 3개를 구입하고 잔돈이 0.3999999...원이 남는다.
- 즉, 반올림 여부와 관계없이 잘못된 값이 나온다.
- 그렇다고 연산 도중에 계속해서 반올림을 하면 결괏값에 큰 영향을 줄 수도 있다.
📌 해결 방법
1️⃣ BigDecimal
💡 금융 계산에는 BigDecimal, int 혹은 long을 사용하라
public class Main {
public static void main(String[] args) {
// BigDecimal을 사용한 해법. 속도가 느리고 쓰기 불편하다.
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought2 = 0;
BigDecimal funds2 = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds2.compareTo(price) >= 0; price = price.add(TEN_CENTS)) {
funds2 = funds2.subtract(price);
itemsBought2++;
}
System.out.println(itemsBought2 + "개 구입");
System.out.println("잔돈(달러): " + funds2); // 잔돈(달러): 0.00
}
}
- BigDecimal 생성자 중에서 부정확한 값이 사용되는 것을 막기 위해 문자열을 받는 생성자 사용했다.
- 올바른 답은 나오지만 2가지 단점이 있다.
- 기본 타입보다 쓰기가 훨씬 불편하고, 느리다.
- 단발성 계산이라면 속도는 무시 가능해도 쓰기 불편하다는 점은 여전하다.
더보기
✒️ BigDecimal
BigDecimal이 얼마나 사용하기 어렵길래? 🤔
그래서 조금 찾아봤는데, 확실히 쉽진 않다.
✨ 사칙 연산
public class Test {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("532.252");
BigDecimal num2 = new BigDecimal("128.119");
System.out.println("더하기 : " + num1.add(num2));
System.out.println("빼기 : " + num1.subtract(num2));
System.out.println("곱하기 : " + num1.multiply(num2));
System.out.println("나누기 : " + num1.divide(num2));
}
}
위 연산은 정상적으로 수행되어야 할 것 같지만 나눗셈에서 오류가 나타난다.
"Non-terminating decimal expansion; no exact representable decimal result."
즉, 소수점을 지정해주지 않아서 정확하게 연산을 할 수가 없단다.
소수점 처리 방법은 다음과 같다. (물론 더 있다.)
- BigDecimal.ROUND_UP : 올림
- BigDecimal.ROUND_DOWN : 버림
- BigDecimal.ROUND_HALF_UP : 반올림 (5 이상 올림)
- BigDecimal.ROUND_HALF_DOWN : 반내림 (5 이하 내림)
System.out.println("나누기 : " + num1.divide(num2, BigDecimal.ROUND_UP)); // 4.155
System.out.println("나누기 : " + num1.divide(num2, BigDecimal.ROUND_DOWN)); // 4.154
System.out.println("나누기 : " + num1.divide(num2, BigDecimal.ROUND_HALF_UP)); // 4.154
System.out.println("나누기 : " + num1.divide(num2, BigDecimal.ROUND_HALF_DOWN)); // 4.154
System.out.println("나누기 : " + num1.divide(num2, 4, BigDecimal.ROUND_UP)); // 4.1544
System.out.println("나누기 : " + num1.divide(num2, 4, BigDecimal.ROUND_DOWN)); // 4.1543
System.out.println("나누기 : " + num1.divide(num2, 4, BigDecimal.ROUND_HALF_UP)); // 4.1544
System.out.println("나누기 : " + num1.divide(num2, 4, BigDecimal.ROUND_HALF_DOWN)); //4.1544
또한 divide 메서드는 기본적으로 피제수의 자리수를 따른다. ⇒ (피제수).divide(제수, 소수점 처리 방법)
만약, 소수점 이하 자리수도 정하고 싶다면 ⇒ (피제수).device(제수, 자릿수, 소수점 처리 방법)
✨ 비교 compareTo
System.out.println("비교 : " + number1.compareTo(BigDecimal.ZERO));
System.out.println("비교 : " + number1.compareTo(new BigDecimal("0")));
// 사용방법
if(number1.compareTo(new BigDecimal("0")) == 1){
// number1 이 0보다 클 경우
}
- compareTo 결과값
- BigDecimal 수치가 val 보다 작으면 -1
- BigDecimal 수치가 val 과 같으면 0
- BigDecimal 수치가 val 보다 크면 1
- BigDecimal Type 끼리만 비교가 가능하다.
- 일반적인 비교 연산자로는 비교할 수 없다.
✨ 소수점 처리
BigDecimal number1 = new BigDecimal("150.35");
System.out.println("소수점 올림 : " + number1.setScale(1, BigDecimal.ROUND_UP));
System.out.println("소수점 버림 : " + number1.setScale(1, BigDecimal.ROUND_DOWN));
System.out.println("소수점 5이상 올림(반올림): " + number1.setScale(1, BigDecimal.ROUND_HALF_UP));
System.out.println("소수점 5이하 내림(반내림): " + number1.setScale(1, BigDecimal.ROUND_HALF_DOWN));
소수점 올림 : 150.4
소수점 버림 : 150.3
소수점 5이상 올림(반올림): 150.4
소수점 5이하 내림(반내림): 150.3
setScale() 메서드를 사용하여 소수점을 움직일 수도 있다.
2️⃣ 기본 타입(int, long)
public class Main {
public static void main(String[] args) {
// int 형을 사용한 해법.
int itemsBought3 = 0;
int funds3 = 100;
for (int price = 10; funds3 >= price; price += 10) {
funds3 -= price;
itemsBought3++;
}
System.out.println(itemsBought3 + "개 구입");
System.out.println("잔돈(센트): " + funds3); //잔돈(센트): 0
}
}
- 성능이 중요하고, 소수점을 직접 추적 가능하며, 숫자가 너무 크지 않다면 기본 타입이 낫다.
- 아홉 자리 십진수는 int, 열여덟 자리 십진수는 long을 사용하라. 그 이상은 BigDecimal을 사용하라.
- 만약 실수값을 다루어야 하는 경우, 연산 순서를 바꾸거나, 정수로 계산 가능한 방법을 고려하라.
- 이번 경우는 달러 대신 센트로 수행하면 정수 타입으로 해결할 수 있는 문제였다.