멘토링

숫자 계산(돈 관련 일)을 할 때 어떤 자료형을 써야 할까?

langsamUndStetig 2022. 6. 10. 21:13

BigDecimal을 사용해야 한다. 

-> 숫자를 정밀하게 저장하고 표현할 수 있기 때문이다.

-> 단점은 느리다.

-> float와 double은 소수점 정밀도에 한계가 있어서, 중요한 계산을 할 때 잘못된 값이 나올 수 있다.

* 그 이유는 내부적으로 수를 저장할 때 이진수의 근사치를 저장해서, 다시 십진수로 표현할 때 값이 이상해지기 때문이다.

** 반면 BigDecimal 타입은 십진수로 수를 저장하여 정밀도가 높다.

public class Practice {
  public static void main(String[] args) {
    double a = 10.0000011111111;
    double b = 3.000001111111111;
    System.out.println(a+b);
    System.out.println(a-b);
    System.out.println(a*b);

  }
}

출력값. 소수점의 값이 이상한 것을 확인할 수 있다,

BigDecimal은 무엇인가

 

1. 자바 api를 검색해보자.

public class BigDecimal extends Number implements Comparable<BigDecimal>

BigDecimal 클래스는 java.math 패키지 안에 들어 있다. Number 클래스를 상속받았으며 Comparable<BigDecimal> 인터페이스를 구현한다.

 

2. BigDecimal 객체를 생성하기 위해선 먼저 java.math.BigDecimal 을 import해줘야 한다. new 키워드를 사용해서 객체를 생성해주면 된다. 생성자에 들어갈 수 있는 값은 String, character array, int, Long double, 그리고 BigInteger다.

*double로 생성할 경우 결과가 이상하게 나올 수 있다. 그 이유는 double이 들어간 생성자는 정확하게 값을 나타냈지만, 기존에 double에 값이 정확하게 표현되지 않았기 때문이다.

 

import java.math.BigDecimal;

public class Practice {
  public static void main(String[] args) {
    BigDecimal big = new BigDecimal(30.23232323232);
    BigDecimal big2 = new BigDecimal("30.23232323232");
    System.out.println(big);
    System.out.println(big2);
  }
}

BigDecimal을 사용했음에도 불구하고, 생성자 매개변수로 double type value를 넣어줄 경우 소수점 이하 값들이 이상하게 나왔다.

3. BigDecimal은 두 부분으로 이루어져있다. unscaled value와 scale. 

 unscaled value는 임의의 정밀성을 가진 정수값이다. Scale은 소수점 아래 자릿수를 나타내는 32 비트 정수다.

예를 들어 BigDecimal 3.14라면 unscaled value는 314고, scale은 2다. 314 * 10^-2

 

4. valueOf()메서드를 사용하여 Long, double 타입을 BigDecimal 객체로 변환할 수 있다. 3번째 메서드는 valueOf( unscaled value, scale) 로 오버로딩되어 있다. 이 메서드는 double 타입을 먼저 String으로 변환한 후 BigDecimal로 변환한다. 생성자를 이용해서 만들기보단 valueOf() method를 이용하자!

BigDecimal b3 = BigDecimal.valueOf(0.1111111131234121521323d); // output 0.11111111312341215
BigDecimal b4 = BigDecimal.valueOf(1212121221L); // output 1212121221
BigDecimal b5 = BigDecimal.valueOf(121212L, 4); // output 12.1212

BigDecimal에서의 계산

 

BigDecimal도 다른 Number 클래스들 (Integer, Long, Double etc.)처럼 산술연산자와 비교 연산자를 제공한다. 또한 scale 조정 연산자와 rounding 그리고 형태 변환 연산자도 제공한다. 

산술연산자 (+, -, /, *) 또는 논리 연산자(>, < etc) 대신 그에 상응하는 메소드들을 사용한다. add, subtract, multiply, divide, 그리고 compareTo. 

그 외에도 다양한 메소드를 제공한다. 그 중 precision, scale, signum이 있다.

BigDecimal은 불변이기 때문에, 이러한 연산의 결과는 기존의 객체를 바꾸지 않고, 새로운 객체를 반환한다.

BigDecimal b5 = BigDecimal.valueOf(121212L, 4); // output 12.1212
System.out.println(b5.precision()); // 6 (모든 숫자 개수)
System.out.println(b5.scale()); // 4 (소수점 뒤에 나온 숫자 개수)
System.out.println(b5.signum()); // 1 (양수인지 음수인지 나타내줌, 음수일 경우 -1)

BigDecimal끼리 서로 값을 비교할 땐 compareTo 메소드를 사용한다. eqauls 메소드도 사용가능하다.

BigDecimal b1 = new BigDecimal("1.01");
BigDecimal b2 = new BigDecimal("1.000");
BigDecimal b3 = new BigDecimal("2.01");
BigDecimal b5 = new BigDecimal("1.00");


System.out.println(b1.compareTo(b2)); // 왼쪽의 값이 오른쪽의 값보다 크면 1
System.out.println(b5.compareTo(b2)); // 동일하면 0
System.out.println(b2.compareTo(b3)); // 오룬쪽 값이 더 크면 -1
//compareTo()메서드는 scale 값을 신경쓰지 않는다. 그래서 1.000과 1.00은 동일한 값으로 여겨진다.

System.out.println(b2.equals(b5)); // equals는 scale 값도 비교하기 때문에 false가 나온다.
BigDecimal b1 = new BigDecimal("1.00");
BigDecimal b2 = new BigDecimal("2.000");


BigDecimal sum = b1.add(b2); // 3.000
BigDecimal subtract = b1.subtract(b2); //-1.000
BigDecimal multiply = b1.multiply(b2); // 2.0000 (소수점 뒤에 0을 하나 더 늘렸더니 개수가 1개 늘어났다. 1.00 * 2.000 -> 2.00000)
BigDecimal divide = b1.divide(b2); // 0.5

 

BigDecimal에서 숫자 반올림하는 법

 

 RoundingMode 클래스와 MathContext 클래스가 있다.

RoundingMode에선 CEILING, FLOOR, UP, DOWN, HALF_UP, HALF_DOWN, HALF_EVEN, UNNECESSARY

*HALF_EVEN은 편향을 최소화하기 때문에 자주 사용되어지고, banker's rounding으로도 유명하다.

*UP, DOWN 과 CEILING, FLOOR의 차이점은 절대값을 붙여서 계산하느냐다. UP과 DOWN은 절대값으로 올림과 내림을 한다. 

MathContext에는 DECIMAL32, DECIMAL 64, DECIMAL128, UNLIMITED가 있다.

인텔리제이에서 가져왔다.

// https://jsonobject.tistory.com/466
// 위 블로그에 적힌 코드를 가져와서 내가 이것저것 실험해봤다.

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class Practice {
  public static void main(String[] args) {
    // 소수점 이하를 절사한다. ,setScale에서 newScale 매개변수의 값을 1로 변경해보니 소수점 1자리까지 나타내졌다. 
    //-1로 변경을 하니 0E+1이 나왔고, -2로 변경을 하니 0E+2가 나왔다.
    // newScale(1, ) -> 출력값: 1.1 newScale(-1, ) -> 출력값: 0E+1
    // 0E+1은 0*10^1을 의미한다. 이것은 1의 스케일을 가진 0을 의미하는 것 같다.
    //음수일 땐 소수점 이하를 절사할 경우 -2가 나온다.
    // RoundingMode.DOWN 메소드를 호출할 경우 -1이 나온다. DOWN은 절대값을 적용한 후 내림을 하기 때문이다.
    BigDecimal b1 =new BigDecimal("1.1234567890").setScale(0, RoundingMode.FLOOR);

// 소수점 이하를 절사하고 1을 증가시킨다.
// 2
    BigDecimal b2 = new BigDecimal("1.1234567890").setScale(0, RoundingMode.CEILING);
// 음수에서는 소수점 이하만 절사한다. UP을 사용한다면 절대값이라고 생각하면 된다. -2가 나온다.
// CEILING을 사용할 경우 -1이 나온다. 이유는 -2 < b3 < -1이기 때문이다. 
   BigDecimal b3 = new BigDecimal("-1.1234567890").setScale(0, RoundingMode.CEILING);

// 소수점 자리수에서 오른쪽의 0 부분을 제거한 값을 반환한다. 0의 개수는 상관없이 다 제거된다.
// 0.9999
   BigDecimal b4 = new BigDecimal("000.99990000").stripTrailingZeros();

// 소수점 자리수를 재정의한다.
// 원래 소수점 자리수보다 작은 자리수의 소수점을 설정하면 예외가 발생한다.
    //더 큰 수의 소수점 자리수를 설정하면 0이 더 붙는다. 6으로 설정시 0.123400
    //원래 소수점 자리수와 동일한 자리수를 설정하면 동일한 값이 출력된다. 0.1234
// java.lang.ArithmeticException: Rounding necessary
 //  BigDecimal b5 = new BigDecimal("0.1234").setScale(3);
   BigDecimal b5 = new BigDecimal("0.1234").setScale(6);

// 반올림 정책을 명시하면 예외가 발생하지 않는다.
    // newScale값을 변경할 경우, 소수점 이하 자리수를 정해주고, 만약에 newScale이 3이라면, 소수점 아래 4번째 자리에서 반올림을 한다.
    // 소수점 이하 자리수를 기존의 값과 동일하거나, 더 크게 지정할 경우, 기존값이 나왔고, 더 큰 경우엔 그 뒤에 정한 자리수만큼 0이 붙어나왔다.
// newScale: 3 -> 출력값: 0.123  | newScale: 5 -> 출력값: 0.12340
    BigDecimal b6 = new BigDecimal("0.1234").setScale(5, RoundingMode.HALF_EVEN);

// 소수점을 남기지 않고 반올림한다.
    //소수점을 남기고 반올림할 경우
	// newScale을 1로 변경하면 출력값: 0.1 ( 0.12를 소수점 둘째 자리에서 반올림하면 0.1이 된다.)
   BigDecimal b7 = new BigDecimal("0.1234").setScale(1, RoundingMode.HALF_EVEN);

// 1 -> newScale 3, 0.990 (한자리 아래의 6이 반올림되면서 10이 되어 값이 올라갔다.)
    // 반올림시 HALF_EVEN 기준을 적용하기 때문에, 한 자리 아래의 값이 5면, 짝수가 되는 방향으로 올림되거나 내림 된다.
   		// 0.9845 -> 0.984   | 0.9855 -> 0.986
   BigDecimal b8 = new BigDecimal("0.9896").setScale(3, RoundingMode.HALF_EVEN);
BigDecimal b1 = new BigDecimal("10.232325");
BigDecimal b2 = new BigDecimal("2022.1234567891234567891234567895128");
BigDecimal rounded = b1.round(MathContext.DECIMAL32); // 7개의 숫자를 표현하고, Rounding_Mode로 HALF_EVEN이 적용.
BigDecimal rounded1 = b2.round(MathContext.DECIMAL128); // 34개의 숫자를 표현하고, Rounding_Mode로 HALF_EVEN이 적용.
BigDecimal rounded2 = b2.round(MathContext.DECIMAL64); // 16개의 숫자를 표현하고, Roundin_Mode로 HALF_EVEN이 적용.
BigDecimal rounded3 = b2.round(MathContext.UNLIMITED); //자리수를 제한하지 않는다. 만약 나눠 떨어지지 않는다면 아래의 예외 발생.
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. 예외가 발생한다

 

*** 내용이 너무나 방대해서 여기까지만 먼저 정리하려고 한다. ***

 

참고 자료

 

https://www.baeldung.com/java-bigdecimal-biginteger

https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/math/BigDecimal.html

https://coding-factory.tistory.com/605

https://jsonobject.tistory.com/466