BigDecimal divide 소수점 - BigDecimal divide sosujeom

728x90

java bigdecimal 나누기 소수점 계산 주의사항

  • bigdecimal 의 생성자는 string 과 double 이 있음.
  • 이 때, double 로 생성자를 만드는건 사용하지 않는게 좋음.
  • 동작이 내가 원하는 방식대로 안될 수가 있음.
  • 그렇기에 bigdecimal 생성자를 생성해서 divide 를 할 때, String 사용하기

BigDecimal bigdecimal = new BigDecimal("12345");
BigDecimal tmp = bigdecimal.divide(new BigDecimal("100");

공유하기

게시글 관리

구독하기Simple is best

  • 카카오스토리
  • 트위터
  • 페이스북

'Java' 카테고리의 다른 글

jsonarray to listmap 변환  (0)2021.01.17java jsonObject to map (json-simple, jackson 사용)  (0)2021.01.15apache httpclient post 방식 한글 깨짐  (0)2021.01.05ConvertUtils stringToMap 설명 및 테스트케이스 작성  (0)2020.12.15InputStream 을 String 으로 변환. Testcase 있음.  (0)2020.12.09

BigDecimal 사칙연산 (더하기, 빼기, 곱하기, 나누기) , 소수점처리(올림, 버림, 반올림) 

, 실수비교 compareTo()함수

1. 사칙연산 및 소수점 처리 ( BigDecimal 사용 )

BigDecimal bdcl1 = new BigDecimal("123.123");

BigDecimal bdcl2 = new BigDecimal("456.456");

더하기 : bdcl1.add(bdcl2);

빼기 : bdcl1.subtract(bdcl2);

곱하기 : bdcl1.multiply(bdcl2);

나누기 : 

올림 - bdcl1.divide(bdcl2 , 2(소수점자리수), BigDecimal.ROUND_UP);

버림 - bdcl1.divide(bdcl2 , 4, BigDecimal.ROUND_DOWN);

반올림 - bdcl1.divide(bdcl2 , 3, BigDecimal.ROUND_HALF_UP);

반내림 - bdcl1.divide(bdcl2 , 1, BigDecimal.ROUND_HALF_DOWN);

2. 실수 비교 ( compareTo()함수 )

동일한 형을 비교 가능(float끼리, double끼리 등)

Float num1 = 2.7;

float num2 = 2.6;

num1.compareTo(num2);

  ㅣ 

비교대상

리턴 값 : 

비교대상이 동일한 값이면 : 0

비교대상이 작은경우 : -1

비교대상이 큰경우 : 1

BigDecimal?

  • BigDecimal은 Java 언어에서 숫자를 정밀하게 저장하고 표현할 수 있는 유일한 방법이다.
  • 소수점을 저장할 수 있는 가장 크기가 큰 타입인 double은 소수점의 정밀도에 있어 한계가 있어 값이 유실될 수 있다.
  • Java 언어에서 돈과 소수점을 다룬다면 BigDecimal은 선택이 아니라 필수이다.
  • BigDecimal의 유일한 단점은 느린 속도와 기본 타입보다 조금 불편한 사용법 뿐이다.

double, 무엇이 문제인가?

  • 소수점 이하의 수를 다룰 때 double 타입은 사칙연산시 아래와 같이 우리가 기대한 값과 다른 값을 출력한다. 이유는 double 타입이 내부적으로 수를 저장할 때 이진수의 근사치를 저장하기 때문이다. 저장된 수를 다시 십진수로 표현하면서 아래와 같은 문제가 발생한다. 아래 설명할 BigDecimal 타입은 내부적으로 수를 십진수로 저장하여 아주 작은 수과 큰 수의 연산에 대해 거의 무한한 정밀도를 보장한다. [관련 링크1] [관련 링크2]
double a = 10.0000;
double b = 3.0000;

// 기대값: 13
// 실제값: 13.000001999999999
a + b;

// 기대값: 7
// 실제값: 6.999999999999999
a - b;

// 기대값: 30
// 실제값: 30.000013000000997
a * b;

// 기대값: 3.33333...
// 실제값: 3.333332555555814
a / b;

BigDecimal 기본 용어

  • // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);
    
    4: 숫자를 구성하는 전체 자리수라고 생각하면 편하나, 정확하게 풀이하면 왼쪽부터 0이 아닌 수가 시작하는 위치부터 오른쪽부터 0이 아닌 수로 끝나는 위치까지의 총 자리수이다.
    // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);
    
    5과 동의어이다. (ex: 012345.67890의 precision은 11이 아닌 9이다.)
  • // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);
    
    6: 전체 소수점 자리수라고 생각하면 편하나, 정확하게 풀이하면 소수점 첫째 자리부터 오른쪽부터 0이 아닌 수로 끝나는 위치까지의 총 소수점 자리수이다.
    // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);
    
    7과 동의어이다. (ex: 012345.67890의 scale은 4이다. 하지만 0.00, 0.0의 scale은 모두 1이다.) BigDecimal은 32bit의 소수점 크기를 가진다.
  • // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);
    
    8: IEEE 754-2008에 의해 표준화된, 부호와 소수점을 수용하며, 최대 34자리까지 표현 가능한 10진수를 저장할 수 있는 형식이다. 2018년 미국 정부의 총 부채액이 15조 7천 500억 달러로 총 14자리 임을 감안하면, 금융권에서 처리되는 대부분의 금액을 수용할 수 있는 크기이다. Java에서는 BigDecimal 타입을 통해 공식적으로 지원한다.

BigDecimal 기본 상수

  • float, double 타입과 다르게 BigDecimal 타입은 초기화가 장황한 편이다. 그래서, 자주 쓰는 0, 1, 100은 쓰기 편하게 미리 상수로 정의되어 있다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN

BigDecimal 초기화

  • double 타입으로 부터 BigDecimal 타입을 초기화하는 방법으로 가장 안전한 것은 문자열의 형태로 생성자에 전달하여 초기화하는 것이다. double 타입의 값을 그대로 전달할 경우 앞서 사칙연산 결과에서 본 것과 같이 이진수의 근사치를 가지게 되어 예상과 다른 값을 얻을 수 있다. [관련 링크]
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);

BigDecimal 비교 연산

  • BigDecimal은 기본 타입이 아닌 오브젝트이기 때문에 특히, 동등 비교 연산을 유의해야 한다. double 타입을 사용하던 습관대로 무의식적으로 == 기호를 사용하면 예기치 않은 연산 결과를 초래할 수 있다.
BigDecimal a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.010");

// 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
// false
a == b;

// 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
// false
a.equals(b);

// 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
// 0
a.compareTo(b);

BigDecimal 사칙 연산

  • Java에서 BigDecimal 타입의 사칙 연산 방법은 아래와 같다. 보다시피 double 타입보다 장황하고 귀찮은 편이다. (아래 설명할 Kotlin에서는 double 타입을 사용하는 것처럼 매우 간결한 문법을 제공한다.)
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");

// 더하기
// 13
a.add(b);

// 빼기
// 7
a.subtract(b);

// 곱하기
// 30
a.multiply(b);

// 나누기
// 3.333333...
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
a.divide(b);

// 나누기
// 3.333
a.divide(b, 3, RoundingMode.HALF_EVEN);

// 나누기 후 나머지
// 전체 자리수를 34개로 제한
// 1
a.remainder(b, MathContext.DECIMAL128);

// 절대값
// 3
new BigDecimal("-3").abs();

// 두 수 중 최소값
// 3
a.min(b);

// 두 수 중 최대값
// 10
a.max(b);

BigDecimal 소수점 처리

  • // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);
    
    9은 Java의 기본 반올림 정책으로 금융권에서 사용하는 Bankers Rounding와 동일한 알고리즘이다. 금융권에서는 시스템 개발시 혼란을 막기 위해 요구사항에 반올림 정책을 명확히 명시하여 개발한다.
// 소수점 이하를 절사한다.
// 1
new BigDecimal("1.1234567890").setScale(0, RoundingMode.FLOOR);

// 소수점 이하를 절사하고 1을 증가시킨다.
// 2
new BigDecimal("1.1234567890").setScale(0, RoundingMode.CEILING);
// 음수에서는 소수점 이하만 절사한다.
// -1
new BigDecimal("-1.1234567890").setScale(0, RoundingMode.CEILING);

// 소수점 자리수에서 오른쪽의 0 부분을 제거한 값을 반환한다.
// 0.9999
new BigDecimal("0.99990").stripTrailingZeros();

// 소수점 자리수를 재정의한다.
// 원래 소수점 자리수보다 작은 자리수의 소수점을 설정하면 예외가 발생한다.
// java.lang.ArithmeticException: Rounding necessary
new BigDecimal("0.1234").setScale(3);

// 반올림 정책을 명시하면 예외가 발생하지 않는다.
// 0.123
new BigDecimal("0.1234").setScale(3, RoundingMode.HALF_EVEN);

// 소수점을 남기지 않고 반올림한다.
// 0
new BigDecimal("0.1234").setScale(0, RoundingMode.HALF_EVEN);
// 1
new BigDecimal("0.9876").setScale(0, RoundingMode.HALF_EVEN);

BigDecimal 나누기 처리

BigDecimal b10 = new BigDecimal("10");
BigDecimal b3 = new BigDecimal("3");

// 나누기 결과가 무한으로 떨어지면 예외 발생
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
b10.divide(b3);

// 반올림 정책을 명시하면 예외가 발생하지 않음
// 3
b10.divide(b3, RoundingMode.HALF_EVEN);

// 반올림 자리값을 명시
// 3.333333
b10.divide(b3, 6, RoundingMode.HALF_EVEN);

// 3.333333333
b10.divide(b3, 9, RoundingMode.HALF_EVEN);

// 전체 자리수를 7개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333
b10.divide(b3, MathContext.DECIMAL32);

// 전체 자리수를 16개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333
b10.divide(b3, MathContext.DECIMAL64);

// 전체 자리수를 34개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333333333333333333333
b10.divide(b3, MathContext.DECIMAL128);

// 전체 자리수를 제한하지 않는다.
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. 예외가 발생한다.
b10.divide(b3, MathContext.UNLIMITED);

Kotlin에서의 BigDecimal

  • Kotlin에서의 BigDecimal 연산은 놀라울 정도로 간단하다. 두 객체 간의 메써드 연산자 대신
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    0,
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    1,
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    2,
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    3,
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    4,
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    5 연산자를 사용하면 된다. 나누기 연산자에 대해서만 유의하면 된다.
val a: BigDecimal = BigDecimal(10)
val b: BigDecimal = BigDecimal(3)
val c: BigDecimal = 3.0.toBigDecimal()

// equalsTo()와 동일하게 소수점 마지막의 0까지 비교
// false
println(b == c)

// compareTo()는 소수점 마지막의 0을 무시
// true
println(b.compareTo(c) == 0)

// 이 경우 == 기호는 equalsTo()가 아닌 compareTo()로 작동
// true
println(b <= c)

// 이 경우 == 기호는 equalsTo()가 아닌 compareTo()로 작동
// true
println(b >= c)

// add()와 동일
// 13.0
println(a + b)

// subtract()와 동일
// 7.0
println(a - b)

// multiply()와 동일
// 30.00
println(a * b)

// divide(other, RoundingMode.HALF_EVEN)와 동일
// 3.3
println(a / b)

// 34자리 정밀도로 나누기 처리
// 3.333333333333333333333333333333333
println(a.divide(b, MathContext.DECIMAL128))

// 연산자 오버로드 선언
// 프로젝트 내에서 동일 연산자 메써드에 대해 1번만 선언 가능
operator fun BigDecimal.div(other: BigDecimal): BigDecimal = this.divide(other, 6, RoundingMode.HALF_EVEN)

// 연산자 오버로드 선언 후 결과 확인
// 3.333333
println(a / b)
  • 위를 응용하면 특정 연산자 오버로드가 선언된 클래스를 아래와 같이 별도로 분리해두는 것이 가독성과 관리 측면에서 좋다.
package com.jsonobject.example

import java.math.BigDecimal
import java.math.RoundingMode

operator fun BigDecimal.div(other: BigDecimal): BigDecimal = this.divide(other, BigDecimalUtils.SCALE_SIX, BigDecimalUtils.BANKERS_ROUNDING_MODE)

class BigDecimalUtils {

    companion object {

        const val SCALE_SIX = 6
        val BANKERS_ROUNDING_MODE = RoundingMode.HALF_EVEN
    }
}
  • 주의할 점은 연산자를 사용하는 클래스 내에서 반드시 오버로드가 선언된 클래스를 import해야 오버로드가 정상적으로 적용된다.
import com.jsonobjet.example.bigdecimal.div

BigDecimal 문자열 변환 출력

  • BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    6을 사용하여 소수점 자리수를 제한하면 원본의 소수점 값은 상실해 버린다. 문자열로 출력하는 것이 목적이라면
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    7 클래스를 사용하는 것이 적합하다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
0

MySQL과 BigDecimal

  • MySQL 또한 Java와 동일한 문제를 가지고 있다. FLOAT, DOUBLE 타입에 소수를 가진 수를 저장할 경우 앞서와 동일한 연산의 정확도 문제가 발생한다. 이를 해결하기 위해 MySQL은 BigDecimal 타입에 대응하는
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    8 타입을 제공한다. 컬럼 선언 방법은 아래와 같다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
1
  • DECIMAL 타입 선언시 괄호 안의 숫자의 의미는 PRECISION, SCALE을 의미한다. (5,2)의 경우 전체 자리수는 5, 소수점 이하 자리수는 2로 선언하겠다는 의미이다.(-999.99~999.99) 괄호를 생략하는 것도 가능한데 이 경우 기본값으로 (10,0)이 적용된다. PRECISION의 최대값은 65로 산업 표준인 DECIMAL128을 충분히 수용할 수 있다. SCALE의 최대값은 30으로 PRECISION보다 클 수 없다.
  • 만약, 지정된 소수 자리수보다 많은 값을 저장할 경우, 지정된 소수 자리수 이하는 절사(floor)된다. [관련 링크] 절사보다 반올림을 선호할 경우, 바로 아래 설명한 애플리케이션 레벨에서의 반올림 처리를 하면 된다.

JPA에서의 BigDecimal 처리

  • JDBC에서 MySQL/MariaDB의
    BigDecimal a = new BigDecimal("2.01");
    BigDecimal b = new BigDecimal("2.010");
    
    // 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
    // false
    a == b;
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
    // false
    a.equals(b);
    
    // 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
    // 0
    a.compareTo(b);
    
    8 타입은
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    0 인터페이스의
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    1,
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    2 2개 메써드로 획득이 가능하다. [관련 링크] JPA 또한 별도의 작업 없이 엔티티 필드에 BigDecimal 타입을 사용하여 처리하면 된다.
  • 만약, 데이터베이스 저장시 소수점 이하 자리수와 반올림 방법을 자동으로 처리되게 하고 싶다면 JPA가 제공하는 커스텀 컨버터를 제작하면 된다. 커스텀 컨버터 작성 예는 아래와 같다. Kotlin으로 작성하였다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
2
  • 작성한 커스텀 컨버터를 엔티티에 적용하는 예는 아래와 같다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
3

Spring Data MongoDB에서의 BigDecimal 처리

  • Spring Data MongoDB는 기본적으로 BigDecimal 타입의 필드를 String 타입으로 저장하고 읽어들인다. 문제는 String 타입의 필드는 도큐먼트 간의 정렬 및 연산시 의도하지 않은 결과를 초래할 수 있다. [관련 링크] 이러한 문제를 예방하려면 String이 아닌 Decimal128(2016-11-29 출시된 MongoDB 3.4부터 지원함에 유의) 타입으로 저장하도록 컨버터를 제작해야 한다. MongoDB의 경우 JPA와는 다르게 개별 필드 레벨로는 컨버터 생성이 불가능하고 별도의 통합된
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    3 커스텀 빈을 작성해야 한다.
  • 첫번째 방법은 아래와 같이 개별 컨버터를 따로 만드는 것이다. 저장할 때와 불러올 때의 컨버터를 각각 따로 만들어야 한다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
4
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
5
  • 앞서 제작한 컨버트를 적용하여 컨버터 빈을 생성하면 첫번째 방법은 적용이 완료된다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
6
  • 두번째 방법은 훨씬 간단하다. CustomConversions 빈만 생성하면 된다. 개별 컨버터를 제작할 필요도 없다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
7

BigDecimal과 Java Stream

// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
8

AtomicBigDecimal

  • Google Guava는 서로 다른 쓰레드에서 하나의 double 변수에 접근시 동시성을 보장하는
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    4을 제공한다. 현재까지, AtomicBigDecimal은 제공되지 않는데 필요할 경우
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    5를 이용하여 아래와 같이 구현할 수 있다.
// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO

// 1
BigDecimal.ONE

// 10
BigDecimal.TEN
9

BigDecimal 타입의 JSON 문자열 변환

  • 지금까지 알아본 BigDecimal의 애플리케이션 내부 연산과 저장소 말고도 신경 써야 할 것이 있다. 바로, 외부 서비스 간의 API 요청-응답 처리이다. JSON 스펙에서는 BigDecimal 타입의 표현 방법에 대해 명확히 규정하고 있지 않다. 그래서 API 응답을 표현할 때 혹시 모를 소수점 이하에서의 데이터 유실을 확실하게 예방하려면 BigDecimal을 숫자가 아닌 문자열로 응답해야 한다. [관련 링크] 아래는 Jackson 라이브러리를 사용하여 POJO-JSON 변환시 소수점 이하 6자리에서 Bankers Rounding을 적용하여 응답하는 커스텀 JsonSerializer를 제작한 예이다.
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);
0
  • 앞서 제작한 JsonSerializer를 아래와 같이 POJO에 명시하면 의도한대로 JSON 응답 처리를 할 수 있다.
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);
1

BigDecimal 유닛 테스트

  • BigDecimal은 JUnit에서 Assertion 기능을 제공하지 않아 유닛 테스트가 불편하다.
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    6 라이브러리를 이용하면 아래와 같이 네이티브하게 BigDecimal에 대한 유닛 테스트를 수행할 수 있다. [라이브러리 링크]
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);
2

BigDecimal 관련 라이브러리

  • BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    7 라이브러리는 java.lang.Math 클래스의 BigDecimal 버전이라고 할 수 있다. BigDecimal 기반 연산과 관련된 여러 유용한 기능을 제공한다. [라이브러리 링크]

BigDecimal과 통화

  • BigDecimal은 돈을 다루는데 있어 가장 확실하고 안전한 타입이다. 하지만, 여러 국가의 통화를 표현하기에는 부족함이 있다. 이러한 통화를 다루기 위한 Java 표준으로 JSR 354가 존재한다. 그리고 이를 구현한 구현체 라이브러리로
    BigDecimal a = new BigDecimal("10");
    BigDecimal b = new BigDecimal("3");
    
    // 더하기
    // 13
    a.add(b);
    
    // 빼기
    // 7
    a.subtract(b);
    
    // 곱하기
    // 30
    a.multiply(b);
    
    // 나누기
    // 3.333333...
    // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    a.divide(b);
    
    // 나누기
    // 3.333
    a.divide(b, 3, RoundingMode.HALF_EVEN);
    
    // 나누기 후 나머지
    // 전체 자리수를 34개로 제한
    // 1
    a.remainder(b, MathContext.DECIMAL128);
    
    // 절대값
    // 3
    new BigDecimal("-3").abs();
    
    // 두 수 중 최소값
    // 3
    a.min(b);
    
    // 두 수 중 최대값
    // 10
    a.max(b);
    
    8가 존재한다. 여러 국가의 통화를 직접 처리하는 것보다 이러한 정식 구현체를 적용하는 것이 효율적이다. [라이브러리 링크]
  • 아래는 build.gradle에 해당 라이브러리의 종속성을 추가하는 방법이다.
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);
3

참고 글