본문 바로가기
TYLER SONGBlog
블로그 목록
CS학습

Floating Point: IEEE 754와 유한 비트로 근사하는 실수 표현

부동소수점은 실수를 부호, 지수, 가수의 조합으로 근사해 유한한 비트에 담는 표현 방식이다. IEEE 754 표준은 이 근사 규칙을 정의해 언어와 하드웨어 사이의 계산 결과를 일치시킨다. 0.1 + 0.2가 0.3이 되지 않는 현상은 버그가 아니라 이 표현 방식의 구조적 한계에서 비롯된다.

송민성5분 읽기

1. 개념

부동소수점(floating point)은 실수를 다음 세 요소의 조합으로 표현하는 방식이다.

  • 부호(sign): 양수/음수
  • 지수(exponent): 소수점 위치를 이동시키는 값
  • 가수(mantissa, fraction): 유효 숫자

값은 대략 (-1)^sign × 1.mantissa × 2^(exponent - bias) 형태로 계산된다. "부동(浮動)"이라는 이름은 소수점 위치가 지수에 따라 움직인다는 뜻이며, 정수부와 소수부 자릿수가 고정된 고정소수점(fixed point)과 대비된다.

IEEE 754는 이 표현의 비트 배치, 반올림 규칙, 특수값(0, 무한대, NaN)을 정의한 국제 표준이며, 오늘날 거의 모든 언어의 float/double이 이 표준을 따른다.

2. 왜 사용하는가

정수 하나로는 소수를 표현할 수 없고, 고정소수점은 표현 범위와 정밀도를 자릿수 배분으로 미리 고정해야 해서 아주 큰 수와 아주 작은 수를 동시에 다루기 어렵다.

부동소수점은 지수부를 통해 소수점 위치를 유동적으로 옮길 수 있어, 같은 비트 수로 매우 넓은 범위(예: double은 약 10^-308 ~ 10^308)를 다룰 수 있다. 대신 정밀도가 절대 오차가 아니라 상대 오차 형태로 유지된다는 특징을 가진다.

3. 동작 원리

3.1 비트 레이아웃

형식전체 비트부호지수가수bias
single (float32)321823127
double (float64)64111521023

정규화된(normalized) 수는 가수의 최상위 비트가 항상 1이라고 가정하고 이 1을 생략해서 저장한다(암묵적 선행 비트, implicit leading bit). 그래서 52비트로 53비트 정밀도를 표현한다.

3.2 특수값

  • 지수 비트가 전부 0, 가수가 0: ±0
  • 지수 비트가 전부 0, 가수가 0이 아님: 비정규화 수(subnormal), 0 근처를 더 촘촘히 표현
  • 지수 비트가 전부 1, 가수가 0: ±무한대
  • 지수 비트가 전부 1, 가수가 0이 아님: NaN(Not a Number)

3.3 오차의 근본 원인

0.1은 이진수로 0.0001100110011...처럼 무한 반복 소수가 된다. 유한한 가수 비트에 저장하려면 어딘가에서 잘라야 하고, 이 절단이 반올림 오차의 근원이다. 십진수로 딱 떨어지는 값도 이진수로는 딱 떨어지지 않는 경우가 많다.

4. 코드 예제

4.1 비트 구조 직접 확인하기 (Python)

python
import struct def float_to_bits(f: float) -> str: # 64비트 double을 빅엔디안 바이트로 변환 후 비트 문자열로 표시 packed = struct.pack('>d', f) bits = ''.join(f'{byte:08b}' for byte in packed) sign = bits[0] exponent = bits[1:12] mantissa = bits[12:] return f'sign={sign} exponent={exponent} mantissa={mantissa}' print(float_to_bits(0.1)) print(float_to_bits(-1.5)) print(float_to_bits(0.0))

4.2 오차가 실제로 발생하는 지점 (Python)

python
from decimal import Decimal a = 0.1 + 0.2 print(a) # 0.30000000000000004 print(a == 0.3) # False # Decimal은 십진 기반이라 십진수 계산에서는 오차가 없다 b = Decimal('0.1') + Decimal('0.2') print(b) # 0.3 print(b == Decimal('0.3')) # True

4.3 안전한 비교 방법 (TypeScript)

typescript
console.log(0.1 + 0.2); // 0.30000000000000004 console.log(Number.EPSILON); // 2.220446049250313e-16 function nearlyEqual(a: number, b: number, epsilon = Number.EPSILON): boolean { const diff = Math.abs(a - b); const scale = Math.max(Math.abs(a), Math.abs(b), 1); return diff < epsilon * scale; } console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true console.log(0.1 + 0.2 === 0.3); // false

5. 시간 복잡도 또는 성능 특성

부동소수점 사칙연산은 하드웨어 FPU(부동소수점 연산 장치)가 처리하며, 정규화된 수에 대한 덧셈/곱셈은 O(1)로 취급한다. 다만 다음 상황에서는 성능 저하가 발생할 수 있다.

  • 비정규화 수(subnormal) 연산은 CPU가 특수 경로(마이크로코드 등)로 처리하는 경우가 있어 정규화 연산보다 느려질 수 있다.
  • 정수 연산과 부동소수점 연산을 자주 오가는 형변환(type conversion)은 별도의 명령을 소모한다.
  • SIMD(예: SSE, AVX)를 활용하면 여러 부동소수점 값을 한 번에 연산해 처리량을 높일 수 있다.

정확한 사이클 수치는 CPU 마이크로아키텍처마다 다르므로 특정 숫자로 단정하지 않는다.

6. 실무 사용 사례

  • 그래픽스, 물리 시뮬레이션, 머신러닝 연산: float32/float64로 넓은 동적 범위와 속도가 필요한 영역
  • 과학 계산, 신호 처리: double 정밀도가 기본
  • 금융, 회계 시스템: 부동소수점 오차가 금액 불일치로 이어지므로 사용하지 않는다. 대신 Decimal, NUMERIC, 또는 최소 단위(원, cent)를 정수로 저장하는 방식을 쓴다.
sql
-- PostgreSQL: 금액은 FLOAT가 아니라 NUMERIC으로 저장한다 CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, amount NUMERIC(12, 2) NOT NULL -- 정확한 십진 연산 보장 );

7. 주의할 점

  • == 비교 금지: 부동소수점 값은 오차 누적 때문에 직접 동등 비교하면 예상과 다른 결과가 나온다. 허용 오차(epsilon)를 두고 비교한다.
  • 누적 오차: 반복문에서 부동소수점을 계속 더하면 오차가 누적된다. 루프 카운터로 float를 쓰지 않는다.
  • NaN의 특성: NaN !== NaN이 참이다. 정렬이나 비교 로직에서 NaN을 별도로 처리해야 한다.
  • 큰 수와 작은 수를 더할 때: 상대적으로 매우 작은 값은 반올림 과정에서 사라질 수 있다(정밀도 손실, catastrophic cancellation은 뺄셈에서 특히 두드러진다).
  • 금액 계산에는 float/double을 쓰지 않는다.

8. 핵심 정리

  • 부동소수점은 부호·지수·가수 조합으로 실수를 근사하며, IEEE 754가 그 규칙을 표준화한다.
  • 넓은 표현 범위를 얻는 대신 상대 오차라는 근본적 한계를 가진다.
  • 0.1 + 0.2 ≠ 0.3은 표준 동작이며, 비교 시 epsilon 기반 근사 비교를 써야 한다.
  • 금액처럼 정확성이 중요한 도메인은 float 대신 Decimal/NUMERIC이나 정수 기반 최소 단위를 사용한다.
© 2026 Tyler Song