IEEE 754: 부호·지수·가수로 실수를 근사하는 부동소수점 표준
IEEE 754는 컴퓨터가 실수를 유한한 비트로 표현하기 위한 국제 표준으로, 부호·지수·가수 세 부분으로 나눠 값을 저장한다. 이 구조 때문에 0.1 같은 단순한 십진수도 정확히 표현하지 못하는 근본적인 한계가 생긴다. 웹 개발자가 금액 계산이나 데이터 비교에서 겪는 오차 버그 대부분이 이 표준의 특성에서 비롯된다.
1. 개념
IEEE 754는 실수를 이진 부동소수점(floating-point) 형태로 표현하는 방법을 정의한 표준이다. 1985년에 제정되었고 현재 거의 모든 CPU와 프로그래밍 언어가 이 표준을 따른다.
핵심 아이디어는 실수를 다음 형태로 저장하는 것이다.
값 = (-1)^부호 × 1.가수 × 2^(지수 - bias)가장 널리 쓰이는 두 가지 형식은 다음과 같다.
| 형식 | 전체 비트 | 부호 | 지수 | 가수 | 십진 정밀도 |
|---|---|---|---|---|---|
| 단정밀도(single, float32) | 32 | 1 | 8 | 23 | 약 7자리 |
| 배정밀도(double, float64) | 64 | 1 | 11 | 52 | 약 15~17자리 |
JavaScript의 number 타입, Python의 float, PostgreSQL의 double precision은 모두 배정밀도(float64)를 기본으로 사용한다.
2. 왜 사용하는가
부동소수점 이전에는 고정소수점(fixed-point) 방식이 쓰였는데, 소수점 위치가 고정되어 있어서 아주 큰 수와 아주 작은 수를 동시에 표현하기 어려웠다. IEEE 754 이전에는 제조사마다 부동소수점 구현 방식이 달라서 같은 코드를 다른 컴퓨터에서 실행하면 결과가 미세하게 달랐다.
IEEE 754는 다음 문제를 해결한다.
- 이식성: 표준을 따르면 어떤 하드웨어에서 계산해도 동일한 결과가 나온다.
- 범위와 정밀도의 절충: 지수부 덕분에 매우 작은 수(예: 10^-300)와 매우 큰 수(예: 10^300)를 하나의 형식으로 표현할 수 있다.
- 하드웨어 가속: CPU에 FPU(부동소수점 연산 장치)가 내장되어 있어 정수 연산과 비슷한 속도로 실수 연산을 처리한다.
3. 동작 원리
3.1 비트 레이아웃
배정밀도(64비트) 기준으로 비트는 다음과 같이 나뉜다.
[부호 1비트][지수 11비트][가수 52비트]- 부호(sign): 0이면 양수, 1이면 음수.
- 지수(exponent): bias(배정밀도는 1023)를 더한 값으로 저장한다. 실제 지수가 -2면 저장값은 1021(=1023-2)이 된다.
- 가수(mantissa/fraction): 정규화된 수는 항상
1.xxxx형태이므로 맨 앞의 1을 생략하고(암묵적 비트, implicit bit) 소수점 아래 52비트만 저장한다.
3.2 정규화와 정밀도 손실
십진수 0.1은 이진수로 나타내면 0.0001100110011...처럼 무한히 반복되는 수다. 52비트라는 유한한 자리에서 이를 자르기 때문에 반올림 오차가 발생한다. 이것이 0.1 + 0.2 !== 0.3이 되는 근본 원인이다.
3.3 특수값
지수부가 전부 0이거나 전부 1인 경우는 일반 규칙 대신 특수한 의미를 가진다.
| 지수부 | 가수부 | 의미 |
|---|---|---|
| 전부 0 | 0 | ±0 |
| 전부 0 | 0 아님 | 비정규화 수(subnormal), 0에 매우 가까운 값 |
| 전부 1 | 0 | ±Infinity |
| 전부 1 | 0 아님 | NaN(Not a Number) |
NaN은 자기 자신과 비교해도 false를 반환한다는 특이한 성질이 있다(NaN === NaN이 false).
4. 코드 예제
4.1 Python: 비트 구조 직접 확인하기
import struct
def float_to_bits(value: float) -> str:
"""배정밀도 실수를 64비트 이진 문자열로 변환"""
packed = struct.pack('>d', value) # 빅엔디안 8바이트
bits = ''.join(f'{byte:08b}' for byte in packed)
return bits
def decompose(value: float) -> None:
bits = float_to_bits(value)
sign = bits[0]
exponent = bits[1:12]
mantissa = bits[12:]
print(f"값: {value}")
print(f"부호: {sign}")
print(f"지수(11bit, biased): {exponent} (실제 지수: {int(exponent, 2) - 1023})")
print(f"가수(52bit): {mantissa}")
print()
decompose(0.1)
decompose(-1.0)
decompose(0.0)실행 결과 요약: 0.1은 정확히 표현되지 않고 반올림된 근사값으로 저장된다. 지수 부분을 통해 실제 지수(-4)를 확인할 수 있다.
4.2 Python: 부동소수점 오차 확인
a = 0.1 + 0.2
print(a) # 0.30000000000000004
print(a == 0.3) # False
# 올바른 비교 방법: 상대 오차 허용
import math
print(math.isclose(a, 0.3, rel_tol=1e-9)) # True4.3 TypeScript: 오차와 안전한 비교
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// Number.EPSILON을 이용한 안전한 비교
function isCloseEnough(a: number, b: number): boolean {
return Math.abs(a - b) < Number.EPSILON * Math.max(Math.abs(a), Math.abs(b), 1);
}
console.log(isCloseEnough(0.1 + 0.2, 0.3)); // true
// 정수로 안전하게 표현 가능한 최대값
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 (2^53 - 1)5. 성능 특성
- FPU가 내장된 현대 CPU에서 부동소수점 덧셈/곱셈은 정수 연산과 비슷하게 한 클럭 근처에서 처리된다. 다만 나눗셈이나 제곱근 연산은 상대적으로 더 많은 클럭 사이클을 소모한다.
- 배정밀도(float64)는 단정밀도(float32)보다 메모리를 2배 사용하지만, 캐시에 더 많은 데이터를 담아야 하는 대규모 배열 연산(예: 머신러닝 텐서 연산)에서는 float32를 선택해 메모리 대역폭을 절약하는 경우가 많다.
- 비정규화 수(subnormal number) 연산은 일부 CPU에서 일반 정규화 수 연산보다 수십 배 느려질 수 있다. 이 때문에 고성능 수치 연산 라이브러리는 종종 subnormal을 0으로 취급하는 flush-to-zero 모드를 사용한다.
6. 실무 사용 사례
- 금융/화폐 계산: 부동소수점 오차가 누적되면 심각한 문제가 되므로 PostgreSQL의
NUMERIC, Java의BigDecimal, JavaScript의 문자열 기반 라이브러리(예: decimal.js)처럼 고정소수점/임의정밀도 타입을 사용한다.
-- 잘못된 예: double precision은 금액 계산에 부적합
CREATE TABLE bad_orders (amount DOUBLE PRECISION);
-- 올바른 예: NUMERIC은 정확한 십진 연산을 보장
CREATE TABLE good_orders (amount NUMERIC(12, 2));
SELECT 0.1::numeric + 0.2::numeric; -- 정확히 0.3- 그래픽스/게임 엔진: 실시간 렌더링에서는 정밀도보다 속도가 중요하므로 float32를 널리 사용한다.
- 과학 계산/시뮬레이션: 오차 누적이 결과에 큰 영향을 주므로 float64를 기본으로 하고, 필요시 오차 분석(error analysis)을 별도로 수행한다.
7. 주의할 점
- 동등 비교 금지: 부동소수점 값을
==또는===로 직접 비교하지 말고 허용 오차(epsilon)를 사용한 근사 비교를 해야 한다. - 정렬 키로 사용 시 주의: 반올림 오차 때문에 논리적으로 같은 값이 다르게 정렬될 수 있다.
- 누적 연산 오차: 반복문에서 실수를 계속 더하면 오차가 누적된다. 합계가 중요한 계산은 정수 단위(예: 원 대신 전 단위)로 변환하거나 Kahan summation 같은 오차 보정 알고리즘을 고려한다.
- NaN 처리:
NaN은 모든 비교 연산에서false를 반환하므로Number.isNaN()같은 전용 함수로 확인해야 한다. - JSON 직렬화: JSON은 IEEE 754 배정밀도를 기본으로 다루기 때문에 매우 큰 정수(2^53을 넘는 값)를 JSON으로 주고받으면 정밀도가 손실될 수 있다. 이런 경우 문자열로 직렬화하는 것이 안전하다.
8. 핵심 정리
IEEE 754는 실수를 부호·지수·가수 세 부분으로 나눠 유한한 비트에 근사값으로 저장하는 표준이다. 이 근사 저장 방식 때문에 십진 소수를 정확히 표현하지 못하는 구조적 한계가 있으며, 이는 버그가 아니라 표준의 설계상 특성이다. 금액이나 정밀도가 중요한 계산에는 NUMERIC이나 BigDecimal 같은 별도의 정밀 십진 타입을 사용하고, 일반적인 수치 비교에는 항상 허용 오차를 두는 습관이 필요하다.