테마
컴퓨터가 이해하는 정보
컴퓨터는 숫자·글자·명령어를 모두 0과 1의 나열로 다룬다. 같은 비트 묶음도 정수로 읽느냐, 실수로 읽느냐에 따라 전혀 다른 값이 된다. 이 문서는 비트로 정수를 적는 방법과 음수·실수를 적는 방법을 보고, 0.1을 왜 정확히 못 적는지 같은 "이상한 계산 결과"의 원인을 설명한다.
비트와 바이트, 2진수와 16진수
비트(bit)는 0 또는 1 하나를 담는 가장 작은 정보 단위다. 비트 8개를 묶은 것이 바이트(byte)이고, 메모리 주소는 보통 바이트마다 하나씩 붙는다. 비트 n개로는 2의 n제곱 가지 값을 구분할 수 있다. 1바이트라면 256가지다.
비트로 수를 적는 방식이 2진수(binary number)다. 10진수가 자리마다 10배씩 커지듯, 2진수는 자리마다 2배씩 커진다. 2진수 1011은 8 + 0 + 2 + 1 = 11이다.
2진수는 길어서 사람이 읽기 힘들다. 그래서 16진수(hexadecimal)로 줄여 적는다. 16진수는 0~9와 A~F 16개 기호를 쓰고, 2진수 네 자리가 16진수 한 자리에 딱 맞는다. 그래서 1바이트는 16진수 두 자리로 적힌다. 0xFF는 2진수 11111111, 즉 255다. 앞의 0x는 16진수라는 표시다.
음수: 2의 보수
음수를 적는 가장 단순한 생각은 맨 앞 비트를 부호로 쓰는 것이다. 하지만 이렇게 하면 +0과 −0이 따로 생기고, 덧셈 회로와 뺄셈 회로를 따로 만들어야 한다.
그래서 거의 모든 컴퓨터는 2의 보수(two's complement)로 음수를 적는다. 어떤 수의 음수는 모든 비트를 뒤집은 뒤 1을 더해 만든다. 8비트에서 5는 00000101이고, 비트를 뒤집으면 11111010, 1을 더하면 11111011이 −5다. 이렇게 적으면 5 + (−5)를 보통 덧셈으로 계산했을 때 넘친 자리를 버리고 00000000이 된다. 즉 덧셈 회로 하나로 뺄셈까지 처리할 수 있고, 0도 하나뿐이다. 맨 앞 비트는 여전히 부호 역할을 해서, 1이면 음수다.
| 8비트 | 부호 없이 읽으면 | 2의 보수로 읽으면 |
|---|---|---|
01111111 | 127 | 127 (가장 큰 양수) |
00000001 | 1 | 1 |
00000000 | 0 | 0 |
11111111 | 255 | −1 |
11111011 | 251 | −5 |
10000000 | 128 | −128 (가장 작은 음수) |
같은 비트라도 어떤 규칙으로 읽느냐에 따라 값이 달라진다는 점이 이 표의 핵심이다.
8비트 2의 보수가 담을 수 있는 범위는 −128부터 127까지다. 계산 결과가 이 범위를 넘치는 것을 오버플로(overflow)라 한다. 127에 1을 더하면 10000000이 되는데, 이 비트는 2의 보수로 −128이다. 큰 양수가 갑자기 음수로 바뀌는 버그가 이렇게 생긴다. Java의 int는 이렇게 조용히 돌아 넘어가고, C에서 부호 있는 정수의 오버플로는 정의되지 않은 동작(undefined behavior)이다. 표준이 결과를 정하지 않았다는 뜻이라, 컴파일러(compiler)가 이 경우는 없다고 보고 최적화해 예상과 다른 결과가 나올 수 있다. 어느 쪽이든 오류가 나지 않아 알아채기 어렵다.
실수: 부동소수점
실수를 적는 방법은 두 가지다. 고정소수점(fixed point)은 정수부와 소수부의 자릿수를 미리 정해 둔다. 단순하지만 아주 큰 수와 아주 작은 수를 함께 다루기 어렵다.
부동소수점(floating point)은 수를 "부호 × 가수(mantissa) × 2의 지수(exponent)" 꼴로 바꿔 세 부분을 따로 저장한다. 과학 표기법에서 6.02 × 10²³을 적는 것과 같은 생각이다. 가수는 6.02처럼 유효 숫자를 담는 부분이고, 지수는 소수점을 몇 자리 옮길지를 담는 부분이다. 지수를 바꿔 소수점 위치를 옮길 수 있으므로 넓은 범위를 표현한다. 거의 모든 CPU와 언어는 IEEE(Institute of Electrical and Electronics Engineers, 미국 전기전자공학자협회)가 정한 IEEE 754라는 국제 표준 형식을 따른다. 비트를 32개 쓰는 단정밀도(single precision)와 64개 쓰는 배정밀도(double precision)가 있고, 가수 비트가 많을수록 유효 숫자를 더 많이 담아 오차가 줄어든다.
| 형식 | 전체 | 부호 | 지수 | 가수 |
|---|---|---|---|---|
단정밀도(float) | 32비트 | 1 | 8 | 23 |
배정밀도(double) | 64비트 | 1 | 11 | 52 |
문제는 10진수로는 딱 떨어지는 0.1이 2진수로는 끝없이 반복된다는 점이다. 1/3을 10진수로 0.333…처럼 끝까지 못 적는 것과 같다. 2진수로 끝이 나는 소수는 분모가 2의 거듭제곱인 분수뿐인데, 0.1은 1/10이고 10에는 5라는 인수가 있어서 0.000110011…처럼 반복된다. 가수 칸은 유한하므로 0.1은 가장 가까운 근삿값으로 저장된다. 그래서 배정밀도로 0.1 + 0.2를 계산하면 0.3이 아니라 0.30000000000000004가 나온다.
python
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False이 때문에 실수끼리 ==로 비교하지 않고 아주 작은 오차 범위 안인지로 비교한다. 돈처럼 오차가 허용되지 않는 값은 부동소수점 대신 수를 10진수 자릿수 기준으로 저장해 0.1도 정확히 담는 10진 자료형(decimal type, Java의 BigDecimal 등)을 쓰거나, 원·센트 단위의 정수로 저장한다.
이어지는 내용은 컴퓨터가 이해하는 정보 2에서 다룬다.
면접 포인트
- "2의 보수를 쓰는 이유는?" → 덧셈 회로 하나로 뺄셈 처리, 0이 하나. 음수 = 비트 반전 + 1.
- "8비트 2의 보수의 범위는?" → −128 ~ 127. 127에 1을 더하면 −128이 되는 오버플로.
- "0.1 + 0.2가 0.3이 아닌 이유는?" → 0.1이 2진수로 무한 소수라 IEEE 754 근삿값으로 저장. 돈은 10진 자료형·정수.
