부동소수점 처리 질문

안녕하세요, 최근 거래소 api들을 사용하다가 부동소수점 관련해서 문제가 발생하는 것을 발견해서 해결방법에 대해 질문좀 드리려고합니다.

api응답은 string으로 주고있는데, 저는 이것을 받아서 더하거나 곱하는 등의 연산을 해야해서
형변환을 해야합니다. 이때 Convert.ToDouble을 사용해서 double로 형변환을 해주고 있었는데요, 문제는 형변환을 하면서 부동소수점으로 오차가 미세하게 발생해서 api 요청에 실패를 하고 있다는 것입니다…

그래서 해결방법을 찾아보고 고민해봤는데 일단 크게 두가지 방법을 찾아봤습니다.

  1. Decimal 타입 사용
  • 정밀도를 더 높혀서 오차범위를 줄이기
  1. 형변환 한 값에 2를 곱한 후 다시 2로 나누기
  • 더 큰값을 만들고 나눔으로써 오차범위를 줄이기

이 두가지 방법중에 어떤게 괜찮을까요? 혹은 다른 괜찮은 방법이 있다면 알려주시면 감사하겠습니다 !

2 Likes

Decimal로 변환하면 사이즈가 두배가 되니 2를 곱하고 나누는게 나을 것 같습니다.

.NET Core 계열(+5,6,7,8,9)를 사용하신다면 인자 없이 ToString()을 호출할 경우 소수점 15자리 정밀도("G15")로 문자열을 반환합니다.

Double.ToString 메서드 (System) | Microsoft Learn

기본적으로 반환 값은 최대 17자리가 내부적으로 유지 관리되지만 15자리의 정밀도만 포함합니다. 이 instance 값이 15자리보다 크면 예상 숫자 대신 또는 NegativeInfinitySymbol 를 반환 PositiveInfinitySymbol 합니다ToString. 더 정밀도를 필요로 하는 경우 항상 17자리의 정밀도를 반환하는 “G17” 형식 사양 또는 숫자가 해당 정밀도로 표현될 수 있는 경우 15자리를 반환하는 “R” 또는 숫자가 최대 정밀도로만 표현될 수 있는 경우 17자리를 지정 format 합니다.

번역이 조금 이상하네요:sweat_smile:

By default, the return value only contains 15 digits of precision although a maximum of 17 digits is maintained internally. If the value of this instance has greater than 15 digits, ToString returns PositiveInfinitySymbol or NegativeInfinitySymbol instead of the expected number. If you require more precision, specify format with the “G17” format specification, which always returns 17 digits of precision, or “R”, which returns 15 digits if the number can be represented with that precision or 17 digits if the number can only be represented with maximum precision.

따라서 ToString("G17")으로 호출해 보시면 잘림 없이 원본 값을 유지할 수 있을 듯 합니다.

4 Likes

double (과 float)은 {부호, 정수, exponent }로 이뤄진 반면,
decimal 은 {부호, 정수, fractional factor} 로 이뤄져 구성 방식이 다릅니다.

이 구성 방식이 둘 사이의 정밀도 차이보다 훨씬 중요한 차이입니다.

double 의 구성 방식은 간편하기는 하지만, 정확한 소수 값을 가리킬 수 없는 한계가 있습니다. 즉, 아래 코드에서 d 가 가진 데이터는 정확히 0.1을 가리키지 못합니다.

double d = 0.1d;

여기에 2를 곱해본 들, 그 결과 역시 정확히 0.2 를 가리키지 못합니다.

decimal 은 이러한 float, double 의 한계를 해결하기 위해 탄생한 형식이라, 아래의 m은 정확히 0.1을 가리킵니다.

decimal m = 0.1m;

여기에 2를 곱한 결과도 정확히 0.2를 가리킵니다.

double d = 0;
decimal m = 0;

for (int i = 1; i <= 10; i++)
{
   d += 0.1d; 
   m += 0.1m;   
}

// d : 0.99999...
// m : 1

decimal은 한 마디로 금융 계산을 위해서 태어난 것이라 할 수 있어, decimal을 사용하는 게 좋습니다. (속도는 느리지만요)

다만, decimal.ToString() 에는 trailing zero가 포함되는 경우가 있습니다.

var mString = m.ToString(); // mString: "1.00"

이 경우, m이 가진 00 은 산술 계산에 영향을 미치지 않지만, Api 가 문자열에 포함된 trailing zero 에 대해 에러를 뱉어내면 보내기 전에 제거해야 합니다.

9 Likes

속도도 중요한 부분이지만, 그보다 중요한게 정확성이라 데시멀을 쓰는게 맞겠네요
감사합니다!

3 Likes

거래소API를 다루시는데 비트코인 소스는 한번도 보신적이 없으신거 같네요. 비트코인 소스에 정답이 있습니다. 비트코인은 내부적으로는 64비트 정수형으로 처리하고 단지 표시만 소수점인 것처럼 보이게 하고 있으며 알트코인이나 거래소 코드들도 다 마찬가지입니다. long형을 쓰시는게 "정답"입니다

2 Likes

경험상 이게 맞습니다.
double 소수점은 어지간한 잔재주로는 해결이 안됩니다.

일반적으로 말씀 하신 방법으로 처리 하는걸로 알고 있습니다.
(알골 시간에 무한대 숫자 만들었던 악몽이…)

닷넷 같은 경우엔 decimal 위에 말씀하신 내용으로 동작하는 걸로 알고 있는데
아닐까요?