2의 보수(two's complement)는 컴퓨터가 음수를 이진수로 표현하는 방식입니다. 핵심 아이디어는 이겁니다 — 어떤 수의 음수는, 모든 비트를 뒤집은 뒤 1을 더한 값으로 정합니다. 이 한 줄짜리 규칙 덕분에 덧셈 회로 하나로 뺄셈까지 처리할 수 있게 되죠. 오늘날 거의 모든 CPU가 정수를 이렇게 저장합니다.
처음 배울 땐 "왜 이렇게 이상하게?" 싶습니다. 나도 그랬어요. 그런데 그 "이상함"이 하드웨어를 얼마나 단순하게 만드는지 알고 나면 감탄하게 됩니다. 왜 이 방식이 이겼는지부터 보겠습니다.
왜 그냥 부호 비트를 안 쓰나
가장 순진한 방법은 맨 앞 비트를 부호로 쓰는 것(부호-크기 표현)입니다. 그런데 여기엔 두 가지 골칫거리가 있습니다.
- 0이 두 개 —
+0과-0이 따로 생깁니다. 비교 로직이 지저분해지죠. - 덧셈기가 복잡 — 부호가 다른 두 수를 더하려면 크기를 비교해 빼는 별도 회로가 필요합니다.
2의 보수는 이 둘을 한 번에 없앱니다. 0은 하나뿐이고, 부호에 상관없이 그냥 이진 덧셈을 하면 정답이 나옵니다. 오버플로로 넘어가는 최상위 캐리 비트를 버리기만 하면 됩니다. 이게 승리의 이유입니다.
직접 계산해 보기 (8비트 기준)
-5를 8비트로 만들어 봅시다. 먼저 +5 = 00000101. 모든 비트를 뒤집으면 11111010, 여기에 1을 더하면 11111011. 이게 -5입니다.
검산은 5 + (-5)가 0이 되는지로 합니다.
00000101 (+5)
+ 11111011 (-5)
-----------
100000000 9비트째 캐리는 버림
= 00000000 (0) ✓
덧셈 회로 하나로 뺄셈이 됐습니다. a - b는 그냥 a + (~b + 1)이니까요. 이 우아함이 2의 보수의 전부입니다.
최상위 비트의 진짜 의미
2의 보수에서 맨 앞 비트는 "부호 표시"가 아니라 음의 자릿값입니다. 8비트라면 각 비트의 가중치가 이렇습니다.
| 비트 위치 | 가중치 |
|---|---|
| 최상위(7번) | -128 (음수!) |
| 6번 | +64 |
| ... | ... |
| 0번 | +1 |
그래서 8비트 범위가 -128 ~ +127로 비대칭입니다. 음수 쪽이 하나 더 많죠. 여기서 유명한 함정이 나옵니다. -128은 있는데 +128은 없으니, -(-128)을 계산하면 다시 -128이 됩니다. 절댓값 함수가 최솟값에서 이상하게 도는 이유가 이겁니다.
실무에서 튀어나오는 자리들
이론 같지만 코드에서 자주 마주칩니다.
- 정수 오버플로 —
int최댓값에 1을 더하면 최솟값(음수)으로 랩어라운드합니다. C에서 부호 있는 오버플로는 정의되지 않은 동작(UB)이라 더 위험합니다. - 부호 있는/없는 비교 —
-1을 부호 없는 정수로 해석하면0xFFFFFFFF, 즉 아주 큰 수가 됩니다. C의int와size_t를 섞어 비교하다 무한 루프가 도는 고전 버그죠. - 산술 우측 시프트 — 부호 있는 음수를 오른쪽으로 밀면 빈 자리를 1(부호)로 채웁니다(부호 확장). 이게 2로 나눈 것과 얼추 맞아떨어집니다.
나는 임베디드 코드에서 센서값을 uint8_t로 받아 빼다가, 결과가 음수여야 하는데 255 근처의 거대한 값이 나오는 버그를 잡느라 반나절을 태운 적이 있습니다. 타입의 부호성을 착각하면 2의 보수가 조용히 복수합니다.
해시 함수나 체크섬 코드가 상수를 0xFFFFFFFF 같은 큰 16진수로 쓰는 것도 같은 원리입니다. 그 값을 부호 있는 32비트로 해석하면 -1이거든요. 언어가 이 비트열을 부호 있는 정수로 볼지 없는 정수로 볼지에 따라 >>(부호 확장)와 >>>(0 채움)의 결과가 갈립니다. 자바스크립트가 비트 연산에서 유독 헷갈리는 것도, 숫자를 내부적으로 32비트 부호 있는 정수로 변환해 다루기 때문입니다.
자주 묻는 질문
1의 보수와 뭐가 다른가요?
1의 보수는 비트를 뒤집기만 합니다(마지막 +1이 없음). 그래서 -0이 따로 생기고, 덧셈 후 캐리를 다시 더해주는 "end-around carry" 보정이 필요합니다. 2의 보수는 +1을 붙여 이 두 문제를 없앤 개선판이고, 그래서 현대 CPU가 전부 2의 보수를 씁니다.
어떤 수가 음수인지 어떻게 아나요?
부호 있는 타입이라는 전제 하에, 최상위 비트가 1이면 음수입니다. 다만 그 비트 하나만으로는 부족하고 "이 값이 부호 있는 타입이냐"라는 해석이 있어야 합니다. 같은 비트열 11111011이 부호 있는 -5일 수도, 부호 없는 251일 수도 있으니까요.
왜 범위가 -128~127처럼 비대칭인가요?
표현 가능한 비트 조합은 256개인데 그중 하나를 0이 차지합니다. 0을 양수 쪽에 포함시켜 세면 양수(0 포함) 128개, 음수 128개로 나뉘어, 음수 최솟값이 양수 최댓값보다 절댓값이 1 큽니다. 0이 하나뿐인 대가로 생기는 자연스러운 비대칭입니다.
파이썬에는 왜 이 오버플로가 없나요?
파이썬의 int는 고정 폭이 아니라 필요하면 자릿수를 무한히 늘리는 임의 정밀도 정수라서 랩어라운드가 없습니다. 대신 numpy의 고정 폭 정수형이나 비트 연산을 쓰면 파이썬에서도 2의 보수 동작을 그대로 만나게 됩니다.

댓글 0