정수 표현: 2의 보수(Two's Complement)로 음수를 표현하는 방법
컴퓨터는 정수를 비트 패턴으로 저장하며, 부호 있는 정수는 2의 보수 방식을 사용해 덧셈 회로 하나로 뺄셈까지 처리한다. 이 방식은 오버플로우(overflow)와 언더플로우(underflow) 같은 실무 버그의 근본 원인이기도 하다. 자바스크립트의 Number 타입과 정수 오버플로우 문제를 이해하려면 이 표현 방식을 먼저 알아야 한다.
1. 개념
정수 표현(Integer Representation)은 컴퓨터가 정수를 고정된 비트(bit) 수의 이진수로 저장하는 방식이다. 부호 없는 정수(unsigned integer)는 모든 비트를 크기 표현에 사용하고, 부호 있는 정수(signed integer)는 음수와 양수를 함께 표현해야 하므로 별도의 인코딩 규칙이 필요하다.
현대 대부분의 컴퓨터는 부호 있는 정수를 2의 보수(Two's Complement) 방식으로 표현한다. 과거에는 부호-크기 표현(Sign-Magnitude)과 1의 보수(One's Complement) 방식도 쓰였지만, 두 방식 모두 0이 두 개 존재하고(+0, -0) 회로 설계가 복잡해지는 단점 때문에 사장되었다.
2. 왜 사용하는가
2의 보수를 쓰는 핵심 이유는 하나의 덧셈 회로(adder)만으로 덧셈과 뺄셈을 모두 처리할 수 있기 때문이다. A - B를 A + (-B)로 바꿔 계산할 수 있으면, CPU는 별도의 뺄셈 회로를 만들 필요가 없다.
또한 2의 보수는 0이 단 하나만 존재하고, 부호 판별이 최상위 비트(MSB, Most Significant Bit) 하나만 보면 되므로 하드웨어 구현이 단순하다.
3. 동작 원리
n비트 2의 보수 표현에서 음수 -x는 다음과 같이 구한다.
- x를 이진수로 표현한다.
- 모든 비트를 반전시킨다(1의 보수).
- 결과에 1을 더한다.
8비트 기준으로 5와 -5를 예로 들면:
5 = 0000 0101
반전 = 1111 1010 (1의 보수)
+1 = 1111 1011 (2의 보수, 즉 -5)이 방식의 표현 범위는 n비트일 때 -2^(n-1) ~ 2^(n-1) - 1이다. 8비트라면 -128 ~ 127, 32비트라면 -2,147,483,648 ~ 2,147,483,647이다. 양수보다 음수를 하나 더 표현할 수 있는 이유는 0이 양수 쪽 자리를 하나 차지하기 때문이다.
최상위 비트가 1이면 음수, 0이면 양수로 해석한다. 이때 최상위 비트의 가중치를 양수가 아닌 음수로 계산하면 값을 바로 구할 수 있다.
1111 1011
= -1×2^7 + 1×2^6 + 1×2^5 + 1×2^4 + 1×2^3 + 0×2^2 + 1×2^1 + 1×2^0
= -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1
= -5덧셈 시 오버플로우는 "두 피연산자의 부호가 같은데 결과의 부호가 다를 때" 발생한다. 부호가 다른 두 수를 더할 때는 오버플로우가 절대 발생하지 않는다.
4. 코드 예제
파이썬은 정수 크기가 임의 정밀도(arbitrary precision)라 오버플로우가 없지만, 비트 연산으로 2의 보수 동작을 직접 구현해서 확인할 수 있다.
def to_twos_complement(value: int, bits: int) -> str:
"""정수를 n비트 2의 보수 이진 문자열로 변환"""
if value < 0:
value = (1 << bits) + value # 2^n을 더해 보수 계산
return format(value, f'0{bits}b')
def from_twos_complement(bit_str: str) -> int:
"""n비트 2의 보수 이진 문자열을 정수로 변환"""
bits = len(bit_str)
value = int(bit_str, 2)
if bit_str[0] == '1': # 최상위 비트가 1이면 음수
value -= (1 << bits)
return value
# 8비트 기준 테스트
print(to_twos_complement(5, 8)) # 00000101
print(to_twos_complement(-5, 8)) # 11111011
print(from_twos_complement('11111011')) # -5
# 오버플로우 시뮬레이션: 8비트 signed 범위는 -128 ~ 127
def add_with_overflow_check(a: int, b: int, bits: int = 8) -> tuple[int, bool]:
max_val = (1 << (bits - 1)) - 1 # 127
min_val = -(1 << (bits - 1)) # -128
result = a + b
overflow = result > max_val or result < min_val
if overflow:
# 실제 하드웨어처럼 랩어라운드(wrap-around) 처리
result = ((result - min_val) % (1 << bits)) + min_val
return result, overflow
print(add_with_overflow_check(120, 10)) # (-126, True) -> 오버플로우 발생
print(add_with_overflow_check(50, 20)) # (70, False)자바스크립트에서 32비트 정수 오버플로우를 실제로 관찰하는 예제다.
// 자바스크립트 비트 연산자(|, &, ^, ~, <<, >>)는 내부적으로
// 32비트 signed 정수(2의 보수)로 변환 후 연산한다.
const maxInt32: number = 2147483647;
console.log(maxInt32 | 0); // 2147483647
console.log((maxInt32 + 1) | 0); // -2147483648, 오버플로우로 랩어라운드 발생
// Number.MAX_SAFE_INTEGER를 넘어서면 정밀도 손실이 발생한다
// (2의 보수 오버플로우와는 다른, IEEE 754 부동소수점 표현 한계 문제)
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992, 값이 뭉개짐5. 시간 복잡도 또는 성능 특성
정수의 덧셈, 뺄셈, 비교 연산은 비트 수가 고정된 경우 O(1)이다. CPU는 하드웨어 수준에서 병렬 캐리 전파(carry propagation) 회로를 사용해 32비트나 64비트 정수 연산을 한 클럭 사이클 또는 그에 가까운 시간에 처리한다.
파이썬처럼 임의 정밀도 정수를 지원하는 언어는 다르다. 큰 정수의 덧셈은 O(n), 곱셈은 자리수 n에 대해 나이브 알고리즘 기준 O(n²)이며 실제로는 카라추바(Karatsuba) 알고리즘 등을 써서 O(n^1.585) 수준으로 최적화된다.
6. 실무 사용 사례
- 데이터베이스 컬럼 타입 선택: PostgreSQL의
INTEGER(4바이트, -2,147,483,648~2,147,483,647)와BIGINT(8바이트)를 구분해서 써야 한다. 자동 증가 ID 컬럼에INTEGER를 쓰면 트래픽이 많은 서비스에서 몇 년 안에 오버플로우가 나는 사고가 실제로 발생한다.
-- 잘못된 예: 트래픽이 많으면 INTEGER 범위(약 21억)를 빠르게 소진한다
CREATE TABLE orders (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
amount NUMERIC(10, 2) NOT NULL
);
-- 권장: BIGINT로 여유 있게 잡는다
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
amount NUMERIC(10, 2) NOT NULL
);- 네트워크 프로토콜과 파일 포맷: TCP 시퀀스 번호(32비트), 파일 크기 필드 등에서 정수 오버플로우로 인한 보안 취약점(integer overflow vulnerability)이 실제 CVE로 다수 보고된 바 있다.
- 자바스크립트 비트 연산 제약:
|0,>>0같은 트릭으로 부동소수점을 32비트 정수로 강제 변환하는 패턴이 성능 최적화나 타입 강제에 쓰인다.
7. 주의할 점
- 언어별로 정수 오버플로우 처리 방식이 다르다. C/C++의 signed 정수 오버플로우는 정의되지 않은 동작(undefined behavior)이라 컴파일러 최적화에 따라 예측 불가능한 결과가 나올 수 있다. 반면 unsigned 정수 오버플로우는 모듈러 연산으로 정의되어 있다.
- 자바스크립트의
Number는 IEEE 754 배정밀도 부동소수점이라 정수를 표현해도Number.MAX_SAFE_INTEGER(2^53 - 1)를 넘으면 정밀도가 깨진다. 큰 정수가 필요하면BigInt를 써야 한다. - 부호 있는 정수와 부호 없는 정수를 비교하거나 캐스팅할 때 암묵적 형변환으로 버그가 생기기 쉽다. 특히 C 계열 언어에서
int와unsigned int를 비교하면int가unsigned로 변환되면서 의도치 않은 결과가 나온다. - DB 스키마 설계 시 ID 컬럼 타입은 초기부터 여유 있게 잡는 것이 마이그레이션 비용보다 훨씬 싸다.
8. 핵심 정리
정수는 고정 비트 수의 이진수로 저장되며, 부호 있는 정수는 2의 보수 방식으로 표현해 하나의 덧셈 회로로 뺄셈까지 처리한다. n비트 2의 보수의 표현 범위는 -2^(n-1) ~ 2^(n-1)-1이고, 이 범위를 벗어나면 오버플로우가 발생해 값이 반대 부호로 뒤집히는 랩어라운드 현상이 생긴다. 실무에서는 DB 컬럼 타입 선택, 언어별 정수 크기 한계, 부동소수점과 정수 표현의 차이를 정확히 인지하는 것이 중요하다.