나는 컴퓨터가 전기만으로 판단한다는 사실이 잘 안 믿겼다
처음 프로그래밍을 배울 때, if (a && b) 같은 코드를 짜면서 "컴퓨터가 이걸 어떻게 이해하지?"라고 궁금했습니다. 소프트웨어 세계에서는 당연히 쓰는 AND, OR 같은 논리 연산자가, 하드웨어에서는 정말 물리적인 회로로 존재한다는 게 신기했어요.
그냥 "CPU가 알아서 판단해주겠지"라고 생각했는데, 알고 보니 그 "판단"의 정체는 트랜지스터를 조합해서 만든 논리 게이트였습니다. 즉, 전기 신호가 0인지 1인지에 따라 회로가 열리고 닫히면서 "참/거짓"이 결정되는 구조였던 거죠.
나는 이 사실을 받아들이는 데 좀 시간이 걸렸습니다. "컴퓨터가 생각한다"는 표현은 좀 과장이고, 사실은 아주 빠르게 전기 스위치를 켜고 끄면서 규칙대로 전기를 흘려보내는 것에 불과하다는 걸 이해했습니다.
논리 게이트 - 전기로 만든 IF 문
논리 게이트(Logic Gate)는 입력된 전기 신호(0 또는 1)를 받아서, 정해진 규칙에 따라 출력(0 또는 1)을 내보내는 회로입니다. 우리가 코드에서 쓰는 조건문의 물리적 실체라고 받아들였습니다.
처음엔 "그냥 전기 스위치 아니야?"라고 생각했는데, 이 게이트들이 조합되면 덧셈, 뺄셈, 비교, 저장 같은 모든 연산이 가능하다는 걸 알고 충격받았습니다.
AND 게이트 - 둘 다 참이어야 참
규칙: 입력 두 개가 모두 1이어야 출력이 1입니다.
진리표(Truth Table):
| A | B | 출력 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
비유: 금고 문을 열려면 두 개의 열쇠를 동시에 꽂아야 합니다. 하나라도 없으면 열리지 않아요.
코드로 보면:
// JavaScript
if (user.isLogin && user.hasTicket) {
console.log("입장 가능");
}
// 비트 연산자로 직접 표현
let a = 1, b = 1;
console.log(a & b); // 1 (AND 연산)
a = 1, b = 0;
console.log(a & b); // 0
트랜지스터로는 어떻게?: 트랜지스터 두 개를 직렬로 연결합니다. 마치 수도관에 수도꼭지 두 개를 일렬로 설치하는 거죠. 물이 나오려면(1) 두 꼭지를 모두 열어야 합니다.
OR 게이트 - 하나라도 참이면 참
규칙: 입력 중 하나라도 1이면 출력이 1입니다.
진리표:
| A | B | 출력 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
비유: 현관 도어락. 비밀번호를 누르거나 카드키를 대거나 둘 중 하나만 해도 문이 열립니다.
코드로 보면:
if (isWeekend || isHoliday) {
console.log("쉬는 날");
}
let a = 1, b = 0;
console.log(a | b); // 1 (OR 연산)
a = 0, b = 0;
console.log(a | b); // 0
트랜지스터로는 어떻게?: 트랜지스터 두 개를 병렬로 연결합니다. 수도관이 두 갈래로 나뉘어 있고, 어느 한쪽 꼭지만 열어도 물이 나옵니다.
NOT 게이트 - 반대로 뒤집기
규칙: 입력이 0이면 1, 입력이 1이면 0으로 바꿉니다. 청개구리 게이트죠.
진리표:
| A | 출력 |
|---|---|
| 0 | 1 |
| 1 | 0 |
비유: 비상 정지 버튼. 평소엔 전기가 흐르다가(1), 버튼을 누르면(1 입력) 전기가 끊깁니다(0 출력).
코드로 보면:
if (!isPaid) {
console.log("결제하지 않았음");
}
let a = 1;
console.log(~a); // -2 (비트 반전, 정확히는 보수 연산)
console.log(!a); // false (논리 반전)
XOR 게이트 - 서로 다를 때만 참
규칙: 두 입력이 서로 다를 때만 1을 출력합니다. 같으면 0입니다.
진리표:
| A | B | 출력 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
비유: 두 사람이 각자 동전을 던졌을 때, 하나는 앞면이고 하나는 뒷면일 때만 상금을 줍니다. 둘 다 앞면이거나 둘 다 뒷면이면 상금 없음.
코드로 보면:
# Python
a, b = 1, 0
print(a ^ b) # 1 (XOR 연산)
a, b = 1, 1
print(a ^ b) # 0
# 암호화에 자주 쓰임
message = 0b1010
key = 0b1100
encrypted = message ^ key # 0110
decrypted = encrypted ^ key # 1010 (원본 복구)
활용: XOR는 암호화, 패리티 체크, 데이터 무결성 검사 등에 엄청 자주 쓰입니다. 같은 키로 두 번 XOR하면 원본이 복구되는 성질 덕분이죠.
NAND와 NOR 게이트 - 만능 키
NAND = NOT + AND. AND 결과를 반전시킵니다. NOR = NOT + OR. OR 결과를 반전시킵니다.
NAND 진리표:
| A | B | 출력 |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
NOR 진리표:
| A | B | 출력 |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 0 |
충격적인 사실: NAND 게이트 하나만 있으면 나머지 모든 게이트(AND, OR, NOT, XOR)를 다 만들 수 있습니다. NOR도 마찬가지예요.
그래서 실제 CPU 설계할 때 NAND 게이트를 기본 단위로 많이 씁니다. 제조 비용이 싸고 효율적이거든요. 우리가 쓰는 USB 메모리나 SSD의 NAND Flash도 이 구조를 기본 단위로 쓰기 때문에 "NAND"라는 이름이 붙었습니다.
게이트를 조합하면 덧셈기가 된다
논리 게이트를 처음 배울 때 "이게 대체 어디다 쓰이나?"라고 생각했는데, 이걸 조합하면 산술 연산을 할 수 있다는 걸 알고 감동받았습니다.
반가산기(Half Adder)
두 개의 1비트 숫자를 더하는 회로입니다.
예시: 1 + 1 = 10 (이진수)
- Sum(합) = 0
- Carry(올림) = 1
회로 구성:
- Sum = A XOR B
- Carry = A AND B
// 반가산기를 코드로 표현
function halfAdder(a, b) {
const sum = a ^ b; // XOR
const carry = a & b; // AND
return { sum, carry };
}
console.log(halfAdder(1, 1)); // { sum: 0, carry: 1 }
console.log(halfAdder(1, 0)); // { sum: 1, carry: 0 }
전가산기(Full Adder)
반가산기는 이전 자리의 올림(Carry)을 처리하지 못합니다. 전가산기는 세 개의 입력(A, B, Carry-in)을 받아서 합과 올림을 출력합니다.
회로 구성:
- 반가산기 두 개 + OR 게이트 하나
function fullAdder(a, b, carryIn) {
const firstSum = a ^ b;
const firstCarry = a & b;
const sum = firstSum ^ carryIn;
const secondCarry = firstSum & carryIn;
const carryOut = firstCarry | secondCarry;
return { sum, carryOut };
}
console.log(fullAdder(1, 1, 1)); // { sum: 1, carryOut: 1 } (1+1+1=11)
와닿은 점: CPU가 숫자를 더하는 것도 결국 이 전가산기를 여러 개 연결한 거라는 사실. 32비트 덧셈이면 전가산기 32개를 일렬로 연결합니다. 이게 바로 ALU(Arithmetic Logic Unit)의 핵심입니다.
불 대수(Boolean Algebra)와 드 모르간의 법칙
논리 게이트를 다루다 보면 회로를 단순화하고 싶을 때가 있습니다. 여기서 불 대수가 쓰입니다.
드 모르간의 법칙:
NOT(A AND B)=NOT(A) OR NOT(B)NOT(A OR B)=NOT(A) AND NOT(B)
코드로 확인:
let a = true, b = false;
// 법칙 1
console.log(!(a && b) === (!a || !b)); // true
// 법칙 2
console.log(!(a || b) === (!a && !b)); // true
실용적인 활용: 코드 리팩터링할 때 조건문을 단순화하는 데 씁니다.
// 원래 코드
if (!(isLoggedIn && hasPermission)) {
console.log("접근 불가");
}
// 드 모르간 적용
if (!isLoggedIn || !hasPermission) {
console.log("접근 불가");
}
두 번째 코드가 훨씬 읽기 쉽죠. 나는 이 법칙을 알고 나서 조건문 짤 때 자연스럽게 적용하게 됐습니다.
트랜지스터가 수십억 개씩 들어가는 이유
현대 CPU는 트랜지스터를 수십억 개 집적합니다. 예를 들어 Apple M1 칩은 160억 개의 트랜지스터가 들어 있습니다.
왜 이렇게 많이 필요한가?
- 하나의 게이트(AND, OR 등)를 만들려면 트랜지스터 여러 개가 필요합니다.
- 전가산기 하나만 만들어도 트랜지스터 수십 개가 들어갑니다.
- 64비트 연산을 하려면 전가산기 64개가 필요하고, 이게 몇백 개의 트랜지스터로 구성됩니다.
- CPU는 덧셈뿐 아니라 곱셈, 나눗셈, 비트 시프트, 비교 연산, 메모리 제어, 캐시, 파이프라인 등 수많은 회로가 필요합니다.
- 그래서 수십억 개가 들어가는 겁니다.
비유: 레고 블록 하나하나가 트랜지스터라고 생각하면, CPU는 수십억 개의 레고를 조립해서 만든 거대한 성입니다. 하나의 성벽(ALU)을 쌓는 데도 수백만 개의 블록이 들어가는 거죠.
프로그래밍 언어의 IF 문이 하드웨어로 어떻게 번역되나
우리가 짠 코드가 실행되려면 결국 논리 게이트를 통과해야 합니다.
if (x > 5 && y < 10) {
console.log("조건 만족");
}
하드웨어 관점에서 해석:
x > 5: CPU의 비교 회로(Comparator)가 작동합니다. 내부적으로는 뺄셈(x - 5)을 수행하고, 결과가 양수인지 확인하는 논리 게이트 조합이 동작합니다.y < 10: 마찬가지로 비교 회로가 작동합니다.&&: 두 비교 결과를 AND 게이트에 입력합니다.- AND 게이트 출력이 1이면, 프로그램 카운터(PC)가 "조건 만족" 블록으로 점프합니다.
결국 이거였다: 우리가 짠 IF 문도 결국 CPU 어딘가에 있는 AND 게이트, OR 게이트를 지나가는 전기 신호로 변환된다는 사실. 이걸 받아들이니까, 코딩할 때 "내가 지금 하드웨어의 스위치를 제어하고 있구나"라는 느낌이 들었습니다.
비트 연산자로 직접 게이트 다루기
JavaScript나 Python에서도 비트 연산자를 쓰면 논리 게이트를 직접 흉내낼 수 있습니다.
# Python
a = 0b1100 # 12 (이진수)
b = 0b1010 # 10
print(bin(a & b)) # 0b1000 (AND)
print(bin(a | b)) # 0b1110 (OR)
print(bin(a ^ b)) # 0b0110 (XOR)
print(bin(~a)) # -0b1101 (NOT, 보수 연산)
print(bin(a << 1)) # 0b11000 (왼쪽 시프트, *2)
print(bin(a >> 1)) # 0b110 (오른쪽 시프트, /2)
실용 사례:
- 권한 관리:
if (userPermissions & PERMISSION_WRITE) - 플래그 설정:
flags |= FLAG_ACTIVE - 플래그 해제:
flags &= ~FLAG_ACTIVE - 홀짝 판별:
if (n & 1)(마지막 비트가 1이면 홀수)
나는 비트 연산자를 배우고 나서 "아, 이게 바로 하드웨어 레벨의 제어구나"라고 이해했습니다.
간단한 회로 다이어그램
[반가산기 회로]
A ----\
XOR ---- Sum
B ----/
A ----\
AND ---- Carry
B ----/
[전가산기 회로]
A -----\
XOR -----\
B -----/ \
XOR ---- Sum
Cin --------------/
(중간 캐리 계산 생략)
|
OR ----- Carry Out
실제 회로는 훨씬 복잡하지만, 개념적으로는 이런 식으로 XOR, AND, OR 게이트를 조합해서 만듭니다.
ALU(Arithmetic Logic Unit)와의 연결
CPU 안에는 ALU라는 부품이 있습니다. 이게 바로 산술 연산(덧셈, 뺄셈, 곱셈 등)과 논리 연산(AND, OR, NOT 등)을 처리하는 핵심 부품이에요.
ALU의 구성:
- 전가산기 여러 개 (덧셈/뺄셈)
- AND, OR, XOR 게이트 조합 (비트 연산)
- 비교기(Comparator)
- 시프터(Shifter)
코드 한 줄이 ALU를 통과하는 과정:
let result = (a + b) & 0xFF;
a + b: ALU의 가산기 회로가 작동 (전가산기 체인)& 0xFF: ALU의 AND 게이트 회로가 작동
나는 이 흐름을 이해하고 나서, 코드 최적화할 때 "이 연산이 ALU에서 몇 사이클 걸릴까?"를 고민하게 됐습니다.
정리해본다
논리 게이트는 컴퓨터의 가장 기초적인 판단 단위입니다. 우리가 짠 코드의 모든 조건문, 모든 연산은 결국 이 게이트들을 통과하는 전기 신호로 변환됩니다.
- AND: 둘 다 참이어야 참
- OR: 하나라도 참이면 참
- NOT: 반대로 뒤집기
- XOR: 서로 다를 때만 참
- NAND/NOR: 만능 게이트, 이것만으로 모든 게이트를 만들 수 있음
이 게이트들을 조합하면:
- 반가산기, 전가산기 (덧셈)
- 비교기 (대소 비교)
- ALU (모든 산술/논리 연산)
- 결국 CPU 전체
결국 이거였다: 컴퓨터의 "생각"은 생각이 아니라, 트랜지스터로 만든 논리 게이트를 통과하는 전기 신호의 흐름일 뿐이라는 사실. 이걸 받아들이니까, 프로그래밍이 훨씬 더 구체적이고 물리적으로 느껴졌습니다.