파이썬 정수, 컴퓨터가 음수를 저장하는 의외의 방법

파이썬 정수, 컴퓨터가 음수를 저장하는 의외의 방법

0

컴퓨터는 0과 1밖에 모른다. 이 단순한 사실에서 흥미로운 질문이 하나 나온다. 그렇다면 -4 같은 음수는 대체 어떻게 저장할까. 마이너스 기호를 따로 적어두는 칸이라도 있는 걸까. 답은 생각보다 영리하다. 이번 글에서는 파이썬 정수를 가지고 숫자가 메모리에 저장되는 진짜 모습을 파헤쳐 본다. 2진수와 16진수 변환부터, 컴퓨터가 음수를 다루는 2의 보수까지.

파이썬 정수가 메모리에 저장되는 구조를 보여주는 이미지
파이썬 정수가 메모리에 저장되는 구조를 보여주는 이미지

숫자는 컴퓨터가 다루는 자료형 중에서도 가장 기본이다. 연산자를 다룬 파이썬 연산자 완전 정복에서 한 걸음 더 들어가, 이번엔 그 숫자가 0과 1로 어떻게 표현되는지를 본다.

같은 수, 다른 표기법

수를 표현하는 방식을 기수법이라고 한다. 밑수를 몇으로 잡느냐에 따라 같은 숫자도 전혀 다르게 적힌다.

  • 10진수: 0부터 9까지 열 개로 표현. 우리가 일상에서 쓰는 방식이다.
  • 2진수: 0과 1만 사용. 컴퓨터가 실제로 인식하는 방식이다.
  • 16진수: 0~9에 a, b, c, d, e, f를 더해 열여섯 개로 표현한다.

핵심은 이 셋이 서로 다른 수가 아니라, 같은 수의 다른 옷이라는 점이다. 25라는 값은 10진수로 25, 2진수로 11001, 16진수로 19다. 표기만 다를 뿐 가리키는 양은 똑같다.

진수 변환, 손으로 vs 파이썬으로

10진수 25를 2진수로 바꿔보자. 손으로 하려면 2의 거듭제곱의 합으로 쪼개야 한다. 25에서 가장 가까운 2의 거듭제곱 16(2⁴)을 빼면 9가 남고, 9에서 8(2³)을 빼면 1이 남는다. 정리하면 이렇다.

25 = 1×2⁴ + 1×2³ + 0×2² + 0×2¹ + 1×2⁰

앞의 계수만 모으면 11001. 이게 2진수 25다. 그런데 코딩할 때마다 이걸 손으로 계산할 수는 없다. 파이썬은 함수 한 줄로 끝낸다.

bin(25)
# '0b11001'

파이썬 공식 문서에 나오듯, bin()은 정수를 2진수 문자열로 바꾼다. 앞에 붙는 0b는 2진수(binary)를 뜻하는 표시다. 16진수도 마찬가지로 간단하다.

address = 0b00101101
hex(address)
# '0x2d'

0x는 16진수를 뜻한다. 여기서 한 가지 실용적인 이유가 보인다. 8비트를 2진수로 쓰면 여덟 자리가 필요하지만, 16진수로는 두 자리면 충분하다. 메모리 주소를 16진수로 표기하는 게 표준인 이유가 이 가독성 때문이다. 32비트 주소도 2진수 서른두 자리 대신 16진수 여덟 자리로 깔끔하게 줄어든다.

파이썬 정수, 양수는 어떻게 저장되나

컴퓨터는 정수를 1, 2, 4, 8바이트 같은 정해진 크기로 저장한다. 그런데 정수에는 양수와 음수가 있으니, 부호를 표시할 자리가 필요하다. 그래서 맨 앞 비트 하나를 부호 전용으로 쓴다. 맨 앞 비트가 0이면 양수, 1이면 음수다.

25를 1바이트로 저장한다고 해보자. 양수라서 맨 앞 비트는 0, 나머지에 11001을 넣고 빈 자리를 0으로 채우면 0001 1001이 된다. 이 부호 비트 때문에 1바이트로 표현하는 범위가 갈린다. 부호 있는 정수는 -128 ~ 127, 부호 없는 정수는 0 ~ 255다. 같은 8비트라도 음수를 포기하면 양수 범위가 두 배로 넓어진다. 이 트레이드오프는 실제 코딩에서 자료형을 고를 때 두고두고 마주친다.

음수의 비밀, 2의 보수

이제 진짜 질문. 음수는 어떻게 저장할까. 컴퓨터는 보수라는 개념을 쓴다. 보수란 말 그대로 ‘보충해 주는 수’다. 10진수에서 3의 9 보수는 더해서 9가 되는 수, 즉 6이다. 26의 9 보수는 73이고, 여기에 1을 더하면 10 보수인 74가 된다. 각 자릿수를 9에서 빼고 1을 더하는 단순한 규칙이다.

컴퓨터는 2진수의 2의 보수를 쓴다. 구하는 법은 두 단계다. 먼저 모든 비트를 뒤집어 1의 보수를 만들고, 거기에 1을 더한다. 예로 -4를 만들어 보자.

4 = 0000 0100

1의 보수(비트 반전) = 1111 1011

2의 보수(+1) = 1111 1100

그러니까 컴퓨터가 저장하는 -4는 1111 1100이다. 정말 그런지 파이썬으로 확인해 보자.

(-4).to_bytes(1, byteorder='little', signed=True)
# b'\xfc'

to_bytes()는 정수를 메모리에 저장되는 바이트 형태로 보여준다. 첫 인자는 바이트 수, byteorder는 빅 엔디언인지 리틀 엔디언인지, signed는 음수를 허용할지를 정한다. 출력 0xfc를 2진수로 펴면 1111 1100. 앞서 손으로 구한 2의 보수와 정확히 같다. 참고로 파이썬 3.11부터lengthbyteorder에 기본값이 생겨서 (-4).to_bytes(1, signed=True)처럼 더 짧게 쓸 수 있다.

컴퓨터는 왜 굳이 2의 보수를 쓸까

복잡해 보이는데 왜 이 방식을 고를까. 이유가 두 가지다.

첫째, 0이 하나로 깔끔해진다. 만약 맨 앞 비트만 부호로 쓰고 나머지를 그대로 둔다면 0000 0000은 +0, 1000 0000은 -0이 된다. 0이 두 개나 생기는 셈이다. 비트 하나를 낭비할 뿐 아니라, 뺄셈에서 +0과 -0을 비교할 때 엉뚱한 결과가 나올 수 있다. 2의 보수는 0을 단 하나로 만든다.

둘째, 뺄셈을 덧셈으로 처리할 수 있다. 컴퓨터는 9 − 4를 계산할 때 빼지 않는다. 대신 9에 -4(2의 보수)를 더한다.

0000 1001 (9) + 1111 1100 (-4) = 1 0000 0101

맨 앞에 받아올림으로 생긴 1은 버린다. 남은 값은 0000 0101, 즉 5다. 9 − 4가 정확히 나왔다. 덕분에 CPU는 뺄셈 회로를 따로 만들지 않고 덧셈기 하나로 더하기와 빼기를 모두 처리한다. 이 단순함이 2의 보수가 사실상의 표준이 된 이유다.

정리하며

파이썬 정수 하나를 들여다봤을 뿐인데, 그 안에 컴퓨터가 숫자를 다루는 사고방식이 전부 들어 있다. 0과 1만으로 음수까지 표현하는 2의 보수, 가독성을 위한 16진수, 부호 비트가 만드는 범위의 한계까지. 이런 기초가 단단하면 비트 연산이나 저수준 디버깅에서 흔들리지 않는다. AI가 코드를 대신 써주는 시대일수록 이 원리를 아는 사람이 더 강하다는 건 프로그래밍 배우기에서도 짚은 이야기다.

오늘 배운 걸 직접 손에 익혀보자. 파이썬 인터프리터를 열고 좋아하는 숫자를 bin(), hex(), to_bytes()에 넣어보는 거다. 변수와 연산의 기본이 더 궁금하다면 파이썬 기초 연산자와 변수파이썬 주석 다는 법으로 이어가면 좋다. 학습 노트를 도식으로 정리하고 싶다면 Gen Studio로 개념 다이어그램을 만들어 두는 것도 방법이다.

결국 컴퓨터에게 숫자란 마이너스 기호가 아니라 비트의 패턴이다. 그 패턴을 읽을 줄 알게 되면, 코드가 메모리에서 실제로 무슨 일을 하는지가 보이기 시작한다.

참고 자료

답글 남기기