Exp/Java & Java Script

[Java] 금융권에서 BigDecimal을 보다.

kilog 2024. 5. 19. 20:46
728x90

안녕하세요 ki입니다.

이번 Exp는 저번에 공유한 DecimalFormat과 같이 금융권 도메인에서 꽃이라고 생각하는 BigDecimal입니다.

2024.05.15 - [Exp/Java & Java Script] - [Java] DecimalFormat으로 천 단위에 점을 찍자

 

[Java] DecimalFormat으로 천단위에 점을 찍자

안녕하세요 ki입니다.이번 Exp는 DecimalFormat입니다.저는 은행권 프로젝트를 다수 참여했었는데 그중 많이 사용했던 클래스를 소개하려고 합니다.이전 글에서 설명하면서 말씀드렸던 것 처럼 증명

kkkkt.tistory.com

 

먼저 BigDecimal에 대해 말씀드리기 전에 왜 알고 가야할 기본지식에 대해 설명하고 이번 글에 주제인 BigDecimal에 대해 공유하고자 합니다.

 

부동소수점 타입 float, double 

보통 실수 연산은 기본 자료형인 float과 double을 사용하였습니다.

하지만 연산을 하다 보면 전혀 다른 엉뚱한 결과가 나오는 걸 알 수 있습니다.

        double a = 2.000;
        double b = 3.000;
        System.out.println(a+b);
        System.out.println(0.1+0.2);
        System.out.println(0.1+1.1);
        System.out.println(0.1+0.2 == 0.3);

 

 

위와 같은 문제는 부동소수타입의 대표적인 문제점으로 볼 수 있는데 실수를 표현하는 float과 double이 부동소수점 자료형이라  같은 문제가 발생할 수 있습니다.

 

 

 

고정 소수점 

 

고정소수점 표현방식은 실수를 부호 비트(signed bit), 정수부(integer part)와 소수부(fractional part)로 나누고, 자릿수를 고정하여 실수를 표현하는 방식입니다. 

 

 

예를 들어 7.75라는 실수를 2진수로 변환하면 111.11이 되는데, 이를 각각 지수부와 소수부에 담아 표현합니다. (그림은 32비트) 고정소수점 표현방식은 구현하는 방법이 간단하다는 장점이 있지만, 자릿수가 제한되어 있으므로 표현할 수 있는 수의 범위가 한정적이라는 치명적인 단점이 있습니다. 

 

 

 

부동 소수점

 

고정소수점 표현방식의 단점을 보안한  더 넓은 범위의 실수를 표현하기 위해 부동소수점이라는 개념이 등장하였습니다.  부동소수점 표현방식은 실수를 부호부(sign), 가수부(mantissa), 지수부(exponent)로 나누고, 정규화된(normalized) 값을 각 비트에 나눠 담아 실수를 표현하는 방식입니다.

 쉽게 생각해서 12.3456를 저장한다면 표현식을 0.123456 * 10^2로 변경한 다음, 가수부에는 0.123456을 담고 지수부에는 2를 저장하는 방식입니다. 이해를 돕기 위해 간단하게 말씀드렸지만 실제로는IEEE754 표준에 따라서 지수부에 bias라는 값을 더해주는 과정을 거치게 됩니다.

 

 

 

※IEEE 754 부동소수점 표현방식

2진수 변환 -> 정규화(1.xxx.... *2의 n승) -> bias 합산(127 더하기) -> 2진수 변환값을 가수부에 대입 

  • 2진수 변환: 111.101(2)
  • 정규화: 1.11101(2)×22
  • Exponent: 2(10)+127(10)(bias) =129(10)=10000001(2)
  • Mantassia: 11101(2)

 

문제 발생원인

부동소수점 표현방식은 고정소수점 표현방식에 비해 표현범위가 더 넓지만, 근본적으로 2진수를 사용하므로 여전히 소수를 표현할 때 오차가 발생하게 됩니다. 특정 수가 무한적으로 반복되면 무한적으로 저장할 공간(bit)이 없어 근삿값으로 표현되고 float과 double은 부동소수점 표현방식으로 구현되었기 때문에 이런 문제들이 발생했던 것입니다.

 

 

지금까지 정리하기

  • 부동소수점 타입 float(32bit), double(64bit)
  • float과 double으로 연산할 때 문제 생기는 경우 있음 문제는 부동소수점 표현방식
  • 부동소수점 타입은 bit수가 부족하면 근삿값으로 표현

 

 

 

본론으로


지금까지 기본 자료형인 float과 double대신 Bigdecimal을 사용하는 이유를 알아보았습니다. 본론으로 BigDecimal에 대해 말씀드리겠습니다.

BigDecimal (java.math.*)

 

Java에서 고정 소수점 수학을 위한 클래스입니다. 이 클래스는 정밀도가 높은 숫자 연산을 필요로 할 때 사용됩니다. 주로 금융 업무나 과학계에서 처럼 소수점 이하 자릿수가 중요한 경우에 유용합니다. 공식문서에 설명에는 불변의 성질을 띠며, 임의 정밀도와 부호를 지니는 10진수라고 설명합니다.

 

 

객체 자료형(ObjectTypes)

BigDeclmal은 객체  자료형입니다. 기본 자료형인 int, long, float, double과는 성능 부분에서 차이가 있습니다.

간단히 요약하자면 기본 자료형이 객체자료형 보다 메모리 효율이 좋고, 연산속도가 빠릅니다.

 

 

BigDecimal 생성

// 문자열로 통해 생성
BigDecimal bd1 = new BigDecimal("123.456");
// int로 통해 생성
BigDecimal bd3 = new BigDecimal(123);
// valueOf로 통해 생성 (double 사용 시 추천)
BigDecimal bd4 = BigDecimal.valueOf(123.456);

System.out.println("String: " + bd1);
System.out.println("int: " + bd3);
System.out.println("valueOf: " + bd4);
        
        
// double로 통해 생성 
// double은 위에 설명처럼 근사값을 갖고 있기때문에 생성시 근사값을 그대로 생성되서
// double로 생성하면 근사값이 출력됩니다.
BigDecimal bd2 = new BigDecimal(123.456);
System.out.println("double: " + bd2);

 

 

 

BigDecimal 연산

        BigDecimal bd1 = new BigDecimal("10.5");
        BigDecimal bd2 = new BigDecimal("2.3");

        // 더하기
        BigDecimal sum = bd1.add(bd2);
        System.out.println("더하기: " + sum);

        // 빼기
        BigDecimal difference = bd1.subtract(bd2);
        System.out.println("빼기: " + difference);

        // 곱하기
        BigDecimal product = bd1.multiply(bd2);
        System.out.println("곱하기: " + product);

        // 나누기
        // 소수점 2자리, 반올림
        BigDecimal quotient = bd1.divide(bd2, 2, RoundingMode.HALF_UP);
        System.out.println("나누기: " + quotient);
        
         // 나머지
        BigDecimal remainder = bd1.remainder(bd2);
        System.out.println("나머지: " + remainder);

        // 부호변환
        BigDecimal negate = bd1.negate();
        System.out.println("부호변환: " + negate);

        // 최대값
        BigDecimal max = bd1.max(bd2);
        System.out.println("최대값: " + max);

        // 최솟값
        BigDecimal min = bd1.min(bd2);
        System.out.println("최솟값: " + min);

 

 

BigDecimal 비교

        // unscaled value = 314, scale = 3
        BigDecimal a = new BigDecimal("3.14"); 
        // unsclaed value = 314, scale =4
        BigDecimal b = new BigDecimal("3.140");

        // 주솟값을 비교한다
        // false
        System.out.print("부등호로 비교하기 = ");
        System.out.println(a == b);


        // unscaled value와 scale을 비교(값과 소수점 자리까지 함께 비교)
        // false
        System.out.print("equals로 비교하기 = ");
        System.out.println(a.equals(b));


        // unscaled value만 비교한다 (값만 비교)
        // true
        System.out.print("compareTo로 비교하기 = ");
        System.out.println(a.compareTo(b) == 0);

 

BigDecimal 소수점 처리

    // 0에서 멀어지는 방향으로 올림 
    // 양수인 경우엔 올림, 음수인 경우엔 내림
    UP(BigDecimal.ROUND_UP),
    
    // 0과 가까운 방향으로 내림
    // 양수인 경우엔 내림, 음수인 경우엔 올림
    DOWN(BigDecimal.ROUND_DOWN),
    
    // 양의 무한대를 향해서 올림 (올림)
    CEILING(BigDecimal.ROUND_CEILING),
    
    // 음의 무한대를 향해서 내림 (내림)
    FLOOR(BigDecimal.ROUND_FLOOR),
    
    // 반올림 (사사오입) 
    // 5 이상이면 올림, 5 미만이면 내림
    HALF_UP(BigDecimal.ROUND_HALF_UP),
    
    // 반올림 (오사육입) 
    // 6 이상이면 올림, 6 미만이면 내림
    HALF_DOWN(BigDecimal.ROUND_HALF_DOWN),

    // 반올림 (오사오입, Bankers Rounding)
    // 5 초과면 올리고 5 미만이면 내림, 5일 경우 앞자리 숫자가 짝수면 버리고 홀수면 올림하여 짝수로 만듦
    HALF_EVEN(BigDecimal.ROUND_HALF_EVEN),
    
    // 소수점 처리를 하지 않음
    // 연산의 결과가 소수라면 ArithmeticException이 발생함
    UNNECESSARY(BigDecimal.ROUND_UNNECESSARY);
    
    // 예제
    // unscaled value = 314, scale = 3
        BigDecimal a = new BigDecimal("3.14"); 
        a.setScale(1,RoundingMode.UP);
        a.setScale(1,RoundingMode.DOWN);
        a.setScale(1,RoundingMode.CEILING);
        a.setScale(1,RoundingMode.FLOOR);
        a.setScale(1,RoundingMode.HALF_UP);
        a.setScale(1,RoundingMode.HALF_DOWN);
        a.setScale(1,RoundingMode.HALF_EVEN);
        a.setScale(1,RoundingMode.UNNECESSARY);

 

 

BigDecimal 소수점 처리 (+정밀도를 묶은)

// 7자리 정밀도(자리수를 7개) 및 HALF_EVEN의 반올림 모드
MathContext.DECIMAL32 


// 16자리 정밀도(자리수를 16개) 및 HALF_EVEN의 반올림 모드
MathContext.DECIMAL64


// 34자리 정밀도(자리수를 34개) 및 HALF_EVEN의 반올림 모드
MathContext.DECIMAL128


// 무제한 정밀 산술 (자리수 제한없음)  
MathContext.UNLIMITED

//예제

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

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

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

 

 

 

기타 메서드

// 절대값 반환
BigDecimal absValue = bd1.abs();

// 값에 n제곱
BigDecimal squaredValue = bd1.pow(2);

// 소수점 자리수를 설정하고 반올림 모드를 지정
BigDecimal scaledValue = bd1.setScale(2, RoundingMode.HALF_UP);

// 문자열로 변환
String strValue = bd1.toString();

// 지수 표기법이 아닌 평범한 문자열로 변환합니다.
String plainStrValue = bd1.toPlainString();

// 공학적 표기법 문자열로 변환
String engStrValue = bd1.toEngineeringString();

// BigDecimal 값을 BigInteger로 변환
BigInteger bigIntValue = bd1.toBigInteger();

// 값을 정확하게 BigInteger로 변환 소수 부분이 있을 경우 예외 처리
BigInteger bigIntExactValue = bd1.toBigIntegerExact();

// double, int, long 타입으로 변환합니다.
double doubleValue = bd1.doubleValue();
int intValue = bd1.intValue();
long longValue = bd1.longValue();

 

 

 

번외 ) BigInteger와 BigDecimal의 차이점은 무엇인가

BigDecimal을 공부하며 BigInteger에 대해 볼 수 있는데요. 가장 큰 차이점은 BigInteger는 정수타입에 매우 큰 숫자를 다루고 BigDecimal은 실수를 다루는 차이입니다. 자세한 공통점과 차이점은 다음 Exp는 BigInteger에 대해 공유해보려고 합니다.

 

참고

더보기