개요
요즘 모의 코인 거래 사이트를 만들어보고 있다. 모의 서비스이긴 하지만 금융 거래를 테마로 하고 있기 때문에 무엇보다 데이터 정합성과 확실한 트랜잭션 처리가 중요하다고 생각하고 있다.
모의 주식 사이트를 만들고자 하였지만 24시간 실시간 데이터를 제공받기에 코인이 더 용이하여 타겟을 바꾸게 되었고 이로 인해 소수점 거래가 발생하였다. 알다시피 주식은 일반적으로 정수개의 주식만 거래가 가능하다. 요즘에서야 소수점 거래도 증권사에서 제공하긴 하지만 기본적으로는 1주가 주식의 기본 단위이다.
하지만 코인 거래에서는 1BTC 가 오늘 기준 1억을 호가하며 단위로 사용하기에는 너무 커져버렸다. 그래서 0.00001 BTC 거래처럼 소수점이 많은 것을 흔히 봤을 것이다.
이러한 소수점 가격, 소수점 수량 등을 계산하면서 Double
타입을 사용하고 있었고 부동 소수점으로 인한 데이터 정합성 문제가 떠올랐다.
부동소수점
부동소수점이란 IEEE 에서 개발한 소수 표현의 표준이다. Double
은 8 byte 크기를 가지고 있으며 (부호 (1bit) + 지수 (11bit) + 가수 (52bit) = 64 bit = 8byte) 로 이루어져있다.
123.45 를 부동소수점으로 표현하면 1.2345 * 10^2 로 표현할 수 있다.
여기서 1.2345 가 가수부이며, 제곱을 뜻하는 2가 지수부이다.
정확성 문제
근데 여기서 문제는 컴퓨터는 2진수로 이루어졌다는 것이다.
실제로 0.1
을 2진수로 표현하기 위해서는 0.0001100110011001100110011001100110...
같은 형태이며 이는 정확히 0.1
이 될 수 없다.
그렇기 때문에 아래와 같은 결과가 나오는 것이다.
System.out.println(0.1 + 0.2); // 0.30000000000000004
이러한 오차는 금융권에서 치명적일 수 있다. 사소해보이지만 추후에 정산 시에 모이고 모여 1원이라도 어긋나는 순간 매우 큰 문제가 될 수 있다.
이러한 문제를 방지하고자 BigDecimal
타입을 사용하였다.
BigDecimal
public class BigDecimal extends Number implements Comparable<BigDecimal> {
/**
* The unscaled value of this BigDecimal, as returned by {@link
* #unscaledValue}.
*
* @serial
* @see #unscaledValue
*/
private final BigInteger intVal;
/**
* The scale of this BigDecimal, as returned by {@link #scale}.
*
* @serial
* @see #scale
*/
private final int scale; // Note: this may have any value, so
// calculations must be done in longs
/**
* The number of decimal digits in this BigDecimal, or 0 if the
* number of digits are not known (lookaside information). If
* nonzero, the value is guaranteed correct. Use the precision()
* method to obtain and set the value if it might be 0. This
* field is mutable until set nonzero.
*
* @since 1.5
*/
private transient int precision;
실제 클래스에 접근해보면 주요한 3가지 필드가 있다. 1234.567
을 기반으로 설명하겠다.
- BigInteger intVal : 소수점을 무시한 전체 숫자값이다. 위의 예시에서는
1234567
이다. - int scale : 소수점으로부터 마지막 0이 아닌 자릿수를 의미한다. 위에서는
3
이다. - int precision : 전체 유효 숫자 자리 수이며, 가장 왼쪽의 0과 소수점을 넘어 오른쪽 0 은 제외한다. 위에서는
7
이다.
// 0.123
intVal: 123
scale: 3
precision: 3
// 123.0
intVal: 1230
scale: 1
precision: 4
// 1000.0
intVal: 10000
scale: 1
precision: 5
결국 Double
의 문제점은 2진수로 소수점을 정확하게 표현할 수 없다는 것이다. 이를 해결하기 위해서 1.234567 * 10^3
이 아니라 1234567 * 10^-3
형태를 만들어버리는 것이다. 정수는 2진수로도 정확하게 표현할 수 있기 때문이다.
실제값은 intVal × 10^(-scale) 로 계산할 수 있다. 사칙연산도 정수를 기반으로 진행하기에 정확한 계산이 가능하다.
하지만 원시 타입보다는 느린 성능을 가지고 있기 때문에 상황을 잘 고려해서 사용해야할 것 같다.
결론
- Float, Double 은 소수점 이하를 2진수로 정확히 표현할 수 없다.
- BigDecimal 은 정수 * 10^(-N) 형태를 지녀 정확한 표현 및 사칙 연산이 가능하다.
- 정확한 대신 성능 이슈가 있으니 필요에 따라 선택하자.
'Programming > JAVA' 카테고리의 다른 글
[Java] Volatile? Synchronized? Concurrent? 자바의 동기화 방식 (4) | 2024.11.09 |
---|---|
[JAVA] 정적 파일 위치 설정 및 src/main/resource 경로 ClassLoader 로 읽기 및 Java Reflection 살펴보기 (0) | 2024.07.11 |
[JAVA] JVM 구조 총정리 (0) | 2024.07.10 |
[JAVA] Spring 없이 웹 서버 구축! (4) 라우팅을 위한 Mapper (0) | 2024.07.09 |
[JAVA] Spring 없이 웹 서버 구축! (3) HTTP Request, Response 객체 (0) | 2024.07.09 |