1. "컴퓨터는 융통성이라곤 없는 기계다"
제가 존경하는 멘토는 버그를 못 찾아서 끙끙대는 저에게 항상 이렇게 말씀하셨습니다. "컴퓨터처럼 생각해. 걔네는 '대충'이나 '적당히'를 몰라. 무조건 Yes 아니면 No야."
사람은 "오늘 날씨가 좀 꿀꿀하네"라고 하면 대충 알아듣습니다. 하지만 컴퓨터에게 이런 애매모호함은 존재하지 않습니다. 컴퓨터에게 세상은 딱 두 가지 색깔뿐입니다. 0(꺼짐) 아니면 1(켜짐).
도대체 왜 인간이 만든 최고의 발명품이라는 컴퓨터가, 고작 손가락 두 개(0, 1)밖에 못 쓰는 바보가 되었을까요? 10진법을 쓰는 게 계산하기도 편하고 인간 친화적일 텐데 말이죠.
오늘은 2진법(Binary)이 선택된 현실적인 이유와, 이 단순함을 넘어 우리가 보는 화려한 그래픽과 텍스트가 되는 과정을 '개발자 시점'에서 뜯어봤습니다.
2. 노이즈(Noise)와의 전쟁 - 전압은 춤을 춘다
이론적으로 컴퓨터는 전기로 돌아갑니다. 만약 우리가 인간처럼 10진법 컴퓨터를 만들고 싶다고 칩시다. 0부터 9까지의 숫자를 전압으로 표현해야겠죠.
- 0V = 숫자 0
- 1V = 숫자 1
- ...
- 9V = 숫자 9
자, 여기서 현장의 엔지니어들은 기겁을 합니다. 왜냐하면 현실 세계의 전기는 깨끗하지 않기 때문입니다.
전선이 낡아서 저항이 생기거나, 옆에 강한 모터가 돌아가거나, 날씨가 더워지면 전압은 쉴 새 없이 춤을 춥니다.
내가 5를 보내려고 5V를 쐈는데, 가는 도중에 전압이 떨어져서 4.6V가 도착했습니다.
이걸 받은 쪽에서는 고민에 빠집니다.
"이게 4야? 5야?"
여기서 판단을 잘못하면 500만원 이체할 게 400만원 이체되는 대형 사고가 터집니다. 이것이 바로 아날로그 신호의 취약점, 노이즈(Noise) 문제입니다.
2진법의 강력한 면역력 (Threshold)
엔지니어들은 결단을 내립니다. "복잡하게 쪼개지 말고, 그냥 있냐 없냐(On/Off)로만 가자!"
- 0V ~ 1.5V: "음, 전기가 거의 없네? 이건 0이야."
- 3.5V ~ 5V: "전기가 확실히 차 있네? 이건 1이야."
이렇게 구역(Threshold)을 넓게 잡아버리니 노이즈에 엄청나게 강해집니다. 5V를 보냈는데 노이즈 껴서 3.8V가 도착해도, 컴퓨터는 "음, 어쨌든 높은 구역(High)에 있으니까 1이군!" 하고 찰떡같이 알아듣습니다 (High state).
이 신뢰성(Reliability) 하나 때문에 컴퓨터는 10진법을 포기하고 2진법의 세상을 선택한 것입니다.
3. 비트(Bit)와 바이트(Byte) - 0과 1로 세상을 그리는 법
0과 1 한 칸을 우리는 비트(Bit)라고 부릅니다. 정보의 최소 단위죠. 하지만 비트 하나로는 '전구 켜기/끄기' 말고는 할 수 있는 게 없습니다.
그래서 초기 엔지니어들은 비트를 8개씩 묶어서 다니기로 약속합니다. 이것이 바로 바이트(Byte)입니다.
2^8 = 256 (256가지 경우의 수)
비트 8개를 모으니 0부터 255까지, 총 256가지의 상태를 표현할 수 있게 되었습니다.
아스키(ASCII)와 유니코드(Unicode)
"256개면 영어 알파벳, 숫자, 특수문자 다 넣고도 남습니다." 그래서 만들어진 약속이 아스키(ASCII) 코드입니다.
- 'A' = 65 (
01000001) - 'a' = 97 (
01100001)
하지만 한글이나 한자를 표현하기엔 턱없이 부족했습니다. 그래서 비트를 더 많이 쓰는(2바이트, 3바이트) 유니코드_Unicode (UTF-8)가 등장하게 된 것입니다. 한글 '가'는 UTF-8에서 3바이트를 씁니다.
4. 음수 표현과 2의 보수 (Two's Complement) 깊이 들여다보기
"0과 1밖에 없는데 -5는 어떻게 저장하나요?"
가장 직관적인 방법은 맨 앞 비트를 부호(Sign)로 쓰는 겁니다. (부호 절댓값 방식).
하지만 이 방식은 0이 두 개 (+0, -0)가 되고 연산 회로가 복잡해집니다.
그래서 컴퓨터는 2의 보수법을 씁니다. 원리는 간단합니다.
- 비트를 다 뒤집는다 (NOT).
- 1을 더한다.
-5를 표현하려면: 0000 0101(5) -> 1111 1010 (뒤집기) -> 1111 1011(-5).
이렇게 하면 덧셈 회로 하나만으로 뺄셈까지 완벽하게 처리할 수 있습니다.
$5 + (-5) = 0$이 되는지 볼까요? 0000 0101 + 1111 1011 = 1 0000 0000.
가장 앞자리 올림수(Carry)를 버리면 정확히 0000 0000이 됩니다. 천재적인 설계입니다.
5. 실수 표현 (부동 소수점, IEEE 754) 한 걸음 더
0.1 + 0.2가 0.3이 아닌 이유는?
이진법으로 0.1을 표현하면 무한소수(0.000110011...)가 되기 때문입니다.
컴퓨터 메모리는 유한하므로 적당히 자르면서(반올림) 오차(Epsilon)가 발생합니다.
IEEE 754 표준은 실수를 세 부분으로 쪼개 저장합니다.
- 부호(Sign): 양수/음수.
- 지수(Exponent): 소수점 위치를 어디로 옮길지.
- 가수(Mantissa): 실제 숫자 데이터 (유효숫자).
돈 계산할 때는 절대 float나 double을 쓰지 말고, BigDecimal 같은 전용 라이브러리를 써야 합니다.
금융 시스템에서 자주 다루는 주제이기도 합니다.
6. 비트 연산의 마법 (Bitmasking) 더 알아보기
2진법을 이해하면 코드 레벨에서 비트 연산(Bitwise Operation)을 자유자재로 다룰 수 있습니다.
DB에서 HasShield, HasSword, HasPotion 같은 불리언(Boolean) 컬럼을 각각 만들면 용량 낭비입니다.
이걸 정수 하나(PlayerState)에 비트 플래그로 몰아넣을 수 있습니다.
- Shield: 1 (
001) - Sword: 2 (
010) - Potion: 4 (
100)
검과 방패를 가졌다면? 1 | 2 = 3 (011).
방패가 있는지 확인하려면? if (state & 1) (AND 연산).
이런 기법을 비트마스킹(Bitmasking)이라고 하며, 성능이 생명인 게임 서버나 임베디드에서 필수입니다.
재미있는 비트 해킹 (Bit Manipulate Tricks)
- 홀수/짝수 판별:
x % 2대신x & 1을 씁니다. 1이면 홀수, 0이면 짝수입니다. 나눗셈보다 훨씬 빠릅니다. - 변수 교환 (Swap): 임시 변수(
temp) 없이 두 변수 값을 바꿀 수 있습니다. (XOR Swap Algorithm)a = a ^ b; b = a ^ b; a = a ^ b; - 2의 거듭제곱 확인:
(x & (x - 1)) == 0이면 x는 $2^n$입니다. (예: 2, 4, 8, 16...).
이런 코드들이 여러분의 코드베이스에 녹아 있다면 "이 사람 CS 좀 아네?"라는 소리를 듣게 됩니다.
7. 16진수(Hex)와 BCD, 엔디안(Endianness) 뜯어보기
1) 16진수 (Hexadecimal)
이진수는 너무 깁니다. 1111 1111.
4비트씩 끊어서 F F로 표현하면 가독성이 좋아집니다. 메모리 주소나 컬러 코드(#FFFFFF)에 씁니다.
2) BCD (Binary Coded Decimal)
금융권이나 옛날 메인프레임에서는 2진법의 오차를 없애기 위해, 4비트에 10진수 숫자 하나(0~9)만 딱 넣는 BCD 방식을 쓰기도 합니다. 용량은 낭비되지만 정확도는 완벽합니다.
3) 엔디안 (Endianness)
데이터를 메모리에 저장할 때 "앞에서부터 채울까, 뒤에서부터 채울까?"의 문제입니다.
0x1234라는 2바이트 숫자를 저장할 때:
- Big Endian:
0x12,0x34순서로 저장. 사람이 읽는 순서와 같음. (네트워크 표준, 모토로라 CPU). - Little Endian:
0x34,0x12순서로 거꾸로 저장. (Intel x86 CPU 표준). 이게 안 맞으면 네트워크 통신할 때 데이터를 엉뚱하게 해석하는 대형 사고가 터집니다.htons()(Host to Network Short) 같은 함수를 쓰는 이유가 바로 이 엔디안을 맞춰주기 위해서입니다.
8. 유니코드와 UTF-8의 비밀 파헤치기
초창기 컴퓨터는 미국인들이 만들어서 알파벳만 있으면 됐습니다 (ASCII). 7비트면 충분했죠. 하지만 전 세계 언어를 지원하려다 보니 Unicode가 나왔습니다. "모든 문자에 고유 번호를 부여하자!"
문제는 "가"(U+AC00)는 44032번인데 이걸 이진수로 어떻게 저장하느냐입니다.
그냥 2바이트나 4바이트로 통일하면(UTF-16, UTF-32), 영어 'A' 하나 저장하는 데도 4바이트를 써서 용량 낭비가 심합니다.
그래서 UTF-8 (가변 길이 인코딩)이 탄생했습니다.
- 영어(ASCII): 1바이트 (
0xxxxxxx) - 한글/한자 등: 3바이트 (
1110xxxx 10xxxxxx 10xxxxxx) - 이모지(😀): 4바이트
이 천재적인 설계 덕분에 영어권 문서는 용량을 아끼고, 다국어는 완벽하게 지원하게 되었습니다. 지금 여러분이 보고 있는 웹페이지도 UTF-8 덕분에 깨지지 않고 보이는 것입니다.
9. 보너스 - 32비트 vs 64비트, 그리고 양자 컴퓨터
1) 왜 32비트 윈도우는 램 4GB밖에 못 쓸까?
32비트 CPU는 주소 공간(Address Space)을 $2^$개 가질 수 있습니다. $2^ = 4,294,967,296$ 바이트 $\approx$ 4GB. 주소록에 페이지가 42억 장밖에 없어서, 램을 8GB를 꽂아도 4GB 이후의 주소는 접근할 방법이 없습니다. 반면 64비트는 $2^ \approx 1800$경(Exabytes)까지 주소를 가질 수 있습니다. 사실상 무한대죠.
2) 비트를 넘어서 - 큐비트 (Qubit)
지금까지 우리는 0 아니면 1인 비트(Bit) 세상에 살았습니다. 하지만 양자 컴퓨터(Quantum Computer)는 큐비트(Qubit)를 씁니다. 큐비트는 중첩(Superposition) 상태를 이용하여, 0과 1을 동시에 가질 수 있습니다.
- 전통적 컴퓨터: 00, 01, 10, 11 중 하나만 처리.
- 양자 컴퓨터: 00, 01, 10, 11을 동시에 확률적으로 처리. 미로 찾기를 할 때 우리는 길이 막히면 돌아오지만(Backtracking), 양자 컴퓨터는 모든 길을 동시에 가보는 것과 같습니다. 물론 아직 상용화 되려면 멀었으니, 우리는 당분간 0과 1을 사랑해주면 됩니다.
10. 마치며 - 0과 1 사이의 우주
우리는 고작 전압이 있냐(1) 없냐(0)의 차이로 만들어진 거대한 디지털 환상 속에 살고 있습니다.
지금 여러분이 보는 이 화려한 화면, 감동적인 유튜브 영상, 복잡한 인공지능까지.
모든 것이 결국은 수십억 개의 스위치(Transistor)가 Tx/Rx를 주고받는 대화일 뿐입니다.
가끔 코딩하다가 풀리지 않을 때, 잠시 모니터를 끄고 생각해보세요.
"이건 그저 0과 1일 뿐이야. 내가 이길 수 있어."
컴퓨터는 융통성 없는 바보지만, 그 단순함(Simplicity)을 극한으로 쌓아올려 위대함(Complexity)을 만들었습니다.
우리도 하루하루 작은 0과 1을 쌓아올려, 언젠가 멋진 프로그램을 완성할 수 있기를 바랍니다.
결국 모든 위대한 소프트웨어도 main() 함수 하나에서 시작되었습니다.