0.1은 0.1이 아니다

SELECT VARCHAR_VALUE
FROM TEMP_TBL
WHERE cast(VARCHAR_VALUE AS float) > 0.1;

이 쿼리를 실행하면 ‘0.1’은 조회될까, 조회되지 않을까?




정답은… 조회된다!


엥


부동소수

컴퓨터는 모든 숫자를 0과 1의 이진수로 저장한다.
문제는 우리가 쓰는 십진수의 소수(실수) 중 대부분은 이진수로 정확하게 표현할 수 없는 무한 순환 소수가 된다는 것이다.
컴퓨터는 실수를 표현하기 위해 부동소수 방식을 사용한다. 하지만 메모리 공간이 한정되어 있으므로 무한 소수를 중간에서 잘라 근사치로 저장한다.

그렇기 때문에 ‘0.1’을 실수로 형변환을 하면 0.10000000149...와 같이 근사치로 표현되어 미세하게 큰 값이 저장될 수 있다.

이건 언어나 DB의 문제가 아니라 컴퓨터의 부동소수 표현의 한계이기 때문에 SQL 뿐만이 아니라 Java, Python 등 모든 언어에서 나타난다.

쿼리를 다시 살펴보자

이제 쿼리에서 왜 ‘0.1’이 조회되는지 분석해 보자.

WHERE cast(VARCHAR_VALUE AS float) > 0.1

여기에는 두 개의 근사치가 비교된다.

  1. cast(‘0.1’ AS float) : 0.1의 근사치
  2. 0.1 : DB가 이 리터럴도 float로 해석하여 0.1의 근사치로 만든다.

두 값 모두 0.1의 근사치이지만, 이 두 값이 동일한 비트 패턴으로 저장되지 않을 수 있으며 미세한 변환 과정 차이로 인해 cast('0.1' AS float)0.1보다 아주 미세하게 더 큰 값으로 결정될 수 있다.

결국 cast(VARCHAR_VALUE AS float) > 0.1 조건이 충족되어 ‘0.1’이 예상과 다르게 조회되는 것이다.

실무적 대처 방안

이러한 부동소수 오류는 결제금융, 환율 등 정밀한 계산이 필요한 서비스에 치명적인 오류로 나타날 수 있다.

(DB에서) DECIMAL / NUMERIC 사용

가장 확실하고 근본적인 해결책이다.
DECIMALNUMERIC 타입은 부동소수점 방식이 아닌 정밀한 십진수 표현 방식을 사용하여 소수점을 우리가 아는 그대로 정확하게 저장한다.

WHERE cast(VARCHAR_VALUE AS DECIMAL(4,2)) > 0.10

(금융권에서) 정수 기반 계산

소수점 자체를 저장하지 않고 가장 작은 단위의 정수로 변환하여 저장하고 계산하는 방법이다.
예를 들어, 12.34원 대신 1234전을 저장하고 정수로 비교한다. 이는 부동소수점 문제를 원천 차단하는 가장 안전한 방식이다.

(다른 언어에서) 전용 라이브러리 사용

JavaScript 같은 환경에서 기본 Number 타입은 부동소수 방식을 사용하므로 소수점 계산에서 오류가 나타난다.
때문에 Decimal.js 같은 라이브러리를 사용하여 정확한 십진수 연산을 수행해야 한다.


간단한 개념이지만 실무 중에 생각없이 float로 형변환하다가 어처구니 없는 일이 일어나서 이 참에 부동소수에 대해 정리해보았다. (이래서 개발할 때 생각을 안하면 안되는건데 ㅋㅋ)

부동소수의 한계를 이해하고 올바른 데이터 타입과 연산 방식을 사용하는 것이 견고한 웹 서비스를 구축하는 핵심이 될 것이다.