BigDecimal과 함께 - 정확한 숫자 계산을 Java로 구현하기

BigDeciaml With Java

 이번 Post는 Java에서 금액을 저장할 때 사용하는 방법을 기록한다. 원시형(primitive) 변수로 저장하는 방법과 Java가 지원하는 Class를 정리한다.

double/float

 한국은 소수점 밑으로 금액이 없다. 그래서 int타입 변수로 처리하는 로직을 생각할 것이다. 하지만, 환산과 세율로 인해 소수점 곱셈이 발생하면 int타입으로는 해결할 수 없다. 그러면, 소수점을 지원하는 double과 float을 생각한다. 그런데 double과 float의 사칙연산 결과는 우리가 생각하는 것과 다르다.

@Test
public void 원시형_사칙연산() {
  double a = 0.1;
  double b = 0.2;
  double c = 0.3;

  // double 끼리 사칙연산.
  System.out.println(a + b - c); // 예상 : 0     / 결과 : 5.551115123125783E-17
  System.out.println(a * b * c); // 예상 : 0.006 / 결과 : 0.006000000000000001

  float d = 0.1f;
  float e = 0.2f;
  float f = 0.3f;

  // float 끼리 사칙연산
  System.out.println(d + e - f); // 예상 : 0     / 결과 : 0.0
  System.out.println(d * e * f); // 예상 : 0.006 / 결과 : 0.0060000005

  // 섞어서 사칙연산
  System.out.println(a + e - f); // 예상 : 0     / 결과 : -8.940696738513054E-9
  System.out.println(d * b * f); // 예상 : 0.006 / 결과 : 0.00600000032782555

  double g = 11000;
  double h = 1.15;

  System.out.println(g*h);       // 예상 : 12650.00 / 결과 : 12649.999999999998
}

 double과 float은 값을 저장할 때, 이진수 근사치를 저장한다. 이 근사치를 다시 십진수로 변환할 때 생각과 다른 값이 출력된다. 찾아보니 해결방법으로 2가지가 있었다.

소수점까지 고려한 Double과 Float 을 만든다.

 내가 표현하려는 금액의 소수점까지 고려해서 정수형처럼 계산 후에 소수점을 지정하는 방법이다.

@Test
public void 원시형_대안_사칙연산() {
  
  double a = 1;
  double b = 2;
  double c = 3;
  double d = 0.001;

  System.out.println(a*b*c*d);                  // 예상 : 0.006 / 결과 : 0.006
}

객체를 이용한 계산을 한다.

 많이 사용하는 방법으로 BigDecimal Class가 있다. 거의 무한대의 숫자를 정확한 십진수로 표현할 수 있다. 

BigDecimal

생성

 파라미터는 double이나 float값이 아닌 문자를 사용한다. double/float이 값으로 입력되어 예상하는 결과와 다르게 나타난다.

@Test
public void BigDecima_생성_테스트() {
  BigDecimal createdByString = new BigDecimal("0.001");
  BigDecimal createdByDouble = new BigDecimal(0.001d);

  System.out.println(createdByString);
  System.out.println(createdByDouble);
}

사칙연산

@Test
public void BigDecima_사칙연산_테스트() {
  BigDecimal a = new BigDecimal("0.01");
  BigDecimal b = new BigDecimal("0.02");
  BigDecimal c = new BigDecimal("0.03");

  // 덧셈 : 0.01 + 0.02 + 0.03 = 0.06
  System.out.println(a.add(b).add(c));

  // 덧셈 : 0.01 + 0.02 - 0.03 = 0
  System.out.println(a.add(b).subtract(c));

  // 덧셈 : 0.01 * 0.02 = 0.0002
  System.out.println(a.multiply(b));

  // 나누셈 : (0.01 + 0.02) / 0.03 = 1
  System.out.println(a.add(b).divide(c));

  // 나누셈 : 절대값(0.01 - 0.02 - 0.03) = 0.04
  System.out.println(a.subtract(b).subtract(c).abs());

  // 나누셈 : -(0.01 + 0.02 = 0.03) = -0.06
  System.out.println(a.add(b).add(c).negate());
}

반올림, 올림, 내림.

@Test
public void RoudingMode_테스트() {
  // 반올림
  System.out.println(new BigDecimal("1.55").setScale(1, RoundingMode.HALF_UP)); // 1.6
  
  // 올림
  System.out.println(new BigDecimal("1.51").setScale(1, RoundingMode.UP)); // 1.6
  
  // 버림
  System.out.println(new BigDecimal("1.56").setScale(1, RoundingMode.DOWN)); // 1.5
}

비교

 비교할 때, equals와 compareTo가 있지만, compareTo를 사용하자. compareTo는 10진수 값으로 비교하지만 equals는 객체로 비교한다.

@Test
public void BigDecima_비교_테스트() {
    BigDecimal a = new BigDecimal("0.01");
    BigDecimal b = new BigDecimal("0.02");
    BigDecimal c = new BigDecimal("0.03");

    System.out.println(a.add(b).subtract(c).compareTo(BigDecimal.ZERO) == 0); // true
    System.out.println(a.add(b).subtract(c).equals(BigDecimal.ZERO)); // false
}

JPA

 JPA는 BigDecimal 타입의 컬럼을 지원한다. 선언해서 사용하면 Converter없이 사용가능하다. 다음 Post에서 BigDecimal을 사용한 Application을 만들어 확인해 볼 예정이다.

참고

  • https://jsonobject.tistory.com/466
  • https://blog.leocat.kr/notes/2019/02/25/java-rounding

댓글

이 블로그의 인기 게시물

JPA 와 함께 - 느낀점

Scott 과 함께 - Recursive Query 구현하기