2026. 9. 10. 22:43ㆍ크래프톤 정글/포스팅
CPU는 전압의 높낮이, 즉 0과 1밖에 구분하지 못하는 회로 덩어리다. 그런데 우리는 컴퓨터로 정수도 계산하고, 실수도 계산하고, 문자도 다룬다. 0과 1밖에 모르는 기계가, 어떻게 이렇게 서로 다른 것들을 구분해서 처리하는 걸까?
1. 컴퓨터의 물리적 실체: CPU와 메모리
컴퓨터 안에는 결국 딱 두 종류의 물리적 장치만 있다고 봐도 된다.
CPU — 전압의 높낮이를 0과 1로 해석해서, 정해진 물리 법칙에 따라 그 비트 패턴을 변환하는 회로 장치다. 내부에는 다음과 같은 하위 회로들이 있다.
- ALU(산술논리장치): 정수 덧셈·뺄셈·비교 등을 수행하는 회로
- FPU(부동소수점 장치): 실수(부동소수점) 연산을 전담하는 별도 회로
- 레지스터: 비트를 잠깐 저장하는 아주 작고 빠른 저장 공간
- 디코더: 특정 비트 패턴(opcode)이 들어오면 특정 회로로 신호를 보내는 회로
메모리 — CPU와 마찬가지로 전기 신호를 0과 1로 저장하는 장치다. 다만 레지스터보다 훨씬 크고 느리다. 계산에 쓸 데이터와 계산 결과를 담아두는 대용량 저장 공간 역할을 한다.
중요한 건, 이 두 장치 모두 스스로 "의미"를 판단하지 않는다는 점이다. CPU는 opcode라는 정해진 비트 패턴이 들어오면 미리 배선된 대로 반응할 뿐이고, 메모리는 그냥 비트를 저장하고 꺼내줄 뿐이다. "이 비트가 정수인지 실수인지"에 대한 이해는 이 물리적 장치 어디에도 없다.
2. 프로그래밍 언어란 무엇인가
여기가 핵심 전제다. 프로그래밍 언어는 결국 CPU와 메모리라는 두 물리 장치를 사용하고 조작하기 위한 지시 체계다.
print("hello"), for 반복문, if 조건문처럼 사람이 읽기 편한 코드도, 결국 다 아래와 같은 저수준 동작으로 환원된다.
- LOAD: 메모리의 어느 주소에서 비트뭉치를 CPU 레지스터로 가져온다
- STORE: 레지스터에 있는 비트뭉치를 메모리의 어느 주소에 저장한다
- OPERATE: 레지스터에 있는 두 값을 ALU 또는 FPU에 넘겨 연산하고, 결과를 다시 레지스터에 저장한다
- JUMP: 다음에 실행할 명령어의 위치를 바꾼다 (조건문, 반복문의 정체)
즉 print(), for문 같은 편리한 문법은 이 저수준 지시들을 사람이 매번 손으로 쓰지 않도록 컴파일러가 대신 번역해주는 편의 계층일 뿐이다. 프로그래밍 언어를 배운다는 건 결국 "CPU와 메모리에게 어떤 순서로 무엇을 하라고 시킬지"를 사람이 이해하기 쉬운 형태로 적는 방법을 배우는 것이다.
3. 그래서 데이터 타입이 왜 필요한가
CPU는 비트만 옮길 줄 알지, 그 비트가 무엇을 의미하는지 모른다. 그런데 "정수로 더하라"와 "실수로 더하라"는 물리적으로 완전히 다른 회로(ALU vs FPU)를 켜는 것이다. 그러니 프로그램을 실행하려면 반드시 "이 비트뭉치를 어떤 회로에 넘겨야 하는지"가 어딘가에서 결정되어 있어야 한다. 그 결정의 근거가 되는 것이 바로 타입이다.
데이터 종류별로 "0과 1을 어떤 형식으로 배치할지"에 대한 저장 규칙도 미리 정해져 있다.
- 정수: 2진수 그대로 저장 (부호를 포함해 two's complement 방식 등을 사용)
- 실수: IEEE 754 규칙에 따라 부호(1비트) + 지수(N비트) + 가수(M비트)로 나눠서 저장
- 문자: 아스키, 유니코드 같은 문자 인코딩 표준에 따라 "이 문자 = 이 숫자"로 매핑
이 규칙들은 CPU가 정하는 게 아니라 컴파일러나 표준화 기구가 정한 약속이다. 타입은 프로그래머 편의를 위한 분류 딱지가 아니라, "이 비트로 어떤 회로를 작동시켜야 하는가"를 결정하는 필수 정보다.
4. 컴파일러는 언제, 어떻게 타입을 아는가 — 컴파일 시점에 벌어지는 일
컴파일이란 자연어에 가까운 소스 코드(사람이 읽기 편한 C 코드)를, 기계가 실행할 수 있는 기계어로 번역하는 과정이다. 이 과정에서 정확히 무슨 일이 벌어지는지 짚어보자.
컴파일러는 소스 파일 하나를 처음부터 끝까지 한 번에, 전체 맥락을 가지고 읽는다. float a라는 선언문을 만나는 순간, 컴파일러는 "a는 앞으로 4바이트, IEEE 754 구조로 저장될 것이다"라는 사실을 자기 내부 장부(심볼 테이블)에 기록해둔다.
이후 코드에서 a가 다시 등장할 때마다(a + b, a * 2 등) 컴파일러는 매번 심볼 테이블을 조회해서 a가 float이라는 걸 확인하고, 그에 맞는 opcode(정수용 ADD가 아니라 실수용 ADDSS/FADD)를 선택해 기계어에 박아 넣는다. 만약 소스 코드에 3.89처럼 고정된 값(리터럴)이 있다면, 컴파일러가 그 순간 실제로 3.89를 IEEE 754 비트로 미리 계산해서 실행 파일 안에 넣어둔다.
여기서 가장 중요한 사실은, 타입을 "아는" 지적 작업은 오직 이 컴파일 시점에, 컴파일러에 의해서만 일어난다. 컴파일이 끝나는 순간 심볼 테이블은 버려진다. 남는 건 오직 이미 결정이 끝난 채로 고정된 opcode의 나열, 즉 기계어 파일뿐이다.
정리하면 컴파일 시점에 벌어지는 일은 다음과 같다.
- 소스 코드를 전체 맥락과 함께 읽으며 각 변수의 타입을 심볼 테이블에 기록
- 각 연산마다 타입에 맞는 opcode를 선택
- 리터럴 값들을 정해진 형식(IEEE 754, 2진수 등)의 비트로 미리 변환
- 이 모든 결정을 담은 고정된 명령어 순서, 즉 실행 파일을 만들어냄
참고 — printf("%f", a)의 %f는 왜 따로 필요한가?
- C는 printf 함수를 범용적으로 쓰기 위해 ...(가변 인자)로 설계함 (그렇지 않으면 타입별로 함수를 다 따로 만들어야 함)
→ 그 결과 printf는 컴파일 타임에 인자 타입을 알 수 없는 함수가 됨 (함수 자체에 인자 타입이 정의되어 있지 않으니까)- 또한, printf는 미리 컴파일된 표준 라이브러리라서, 지금 짜는 코드의 심볼 테이블 정보(a는 float이다)에 애초에 접근할 방법도 없음
- 그래서 %f 같은 포맷 지정자로 런타임에 프로그래머가 직접 타입 정보를 전달해야 함
5. 런타임: 이미 정해진 것을 그대로 재생하는 시간
런타임은 컴파일이 끝난 실행 파일이 실제로 CPU와 메모리에 올라가서 물리적으로 동작하는 시점이다. 이때 벌어지는 일은 컴파일 시점과 성격이 완전히 다르다.
- 실행 파일 안에 미리 박혀 있던 비트뭉치(예: 3.89의 IEEE 754 표현)가 실제 메모리의 어느 주소에 복사되어 들어간다.
- CPU는 프로그램 카운터(PC)가 가리키는 명령어를 하나씩 순서대로 가져와 실행한다.
- 이때 CPU는 "이 비트가 정수인지 실수인지" 전혀 판단하지 않는다. 그냥 컴파일 시점에 이미 선택되어 있던 opcode(ADD인지 FADD인지)를 만나는 대로 그 회로를 켤 뿐이다.
- 저장(STORE)과 이동(LOAD)은 비트뭉치를 통째로 복사하는 것뿐이라 타입과 아예 무관하다. 오직 계산(연산) opcode가 실행되는 순간에만 ALU 또는 FPU가 비트를 실제로 "해석"해서 사용한다.
즉 런타임에 CPU가 하는 일은 판단이 아니라 재생이다. "이건 실수니까 FPU를 써야지" 같은 지적 결정은 이미 컴파일 시점에 끝나 있고, 런타임의 CPU는 그 결정이 이미 새겨진 명령어를 그대로 따라갈 뿐이다.
| 컴파일 시점 | 런타임 | |
| 무엇을 하는가 | 소스 코드를 읽고 타입을 판단해 opcode를 결정 | 결정된 opcode를 순서대로 실행 |
| 주체 | 컴파일러 (소프트웨어) | CPU, 메모리 (하드웨어) |
| 타입을 아는가 | 안다 (심볼 테이블을 통해) | 모른다 (그냥 정해진 회로를 켤 뿐) |
| 결과물 | 고정된 명령어 나열 (실행 파일) | 실제 값의 계산, 저장, 출력 |
6. 런타임에 opcode는 실제로 CPU 안에서 어떻게 회로를 움직이는가
방금 "런타임의 CPU는 판단 없이 opcode를 그대로 재생할 뿐"이라고 했는데, 그 "재생"이 물리적으로 정확히 무슨 뜻인지 한 단계 더 들어가 보면 이렇다.
CPU 안에는 성격이 완전히 다른 부품 세 가지가 있다.
- 디코더: 계산은 전혀 하지 않는다. opcode 비트를 읽고 "이번엔 어느 장치를 쓸지, 그 장치 안에서 어느 경로를 열지"를 판단해서 여러 개의 제어 신호를 만들어내는 판단 회로다.
- 레지스터: 계산도 판단도 하지 않는다. 데이터를 잠깐 붙잡아두는 저장 회로다.
- ALU, FPU: 실제로 값을 계산하는 실행 회로다. ALU는 정수·문자(아스키 코드) 연산을, FPU는 실수 연산을 전담한다.
디코더가 만들어내는 제어 신호들은 도착하는 위치에 따라 전혀 다르게 작동한다는 게 핵심이다.
- 큰 단위 선택 (ALU를 쓸지 FPU를 쓸지): 이 신호는 데이터가 흘러가는 통로인 버스의 길목에 도착해서, 데이터 자체가 필요 없는 장치 쪽으로는 아예 흘러들어가지 못하게 막아버린다. 그래서 예를 들어 정수 연산을 할 때는 FPU에 애초에 입력 전압조차 걸리지 않고, 계산 자체가 시작되지 않는다. ALU와 FPU는 그 자체로 크고 전력을 많이 쓰는 장치라, 처음부터 이렇게 걸러내는 편이 이득이다.
- 작은 단위 선택 (ALU 안에서 덧셈이냐 뺄셈이냐): 여기서부터는 이야기가 다르다. ALU 안의 덧셈 회로, 뺄셈 회로, AND 회로, OR 회로는 전부 물리적으로 이미 배선되어 있는 고정된 회로라서, 입력값(A, B)이 걸리는 순간 opcode와 무관하게 전부 동시에, 저절로 계산이 끝나 있다. 디지털 회로에서 "계산이 일어난다"는 건 무언가를 실행시키는 게 아니라, 전압이 걸리면 물리 법칙에 따라 자동으로 결과 전압이 나타나는 것이기 때문이다. 디코더의 제어 신호는 이 결과들 중 하나만 골라서 다음 단계로 통과시키는 멀티플렉서라는 스위치에 도착한다. 나머지 결과들은 이미 계산되어 있지만 그냥 버려진다.
| 선택 단계 | 신호가 하는 일 | 도착 지점 | 낭비 여부 |
| ALU vs FPU | 입력 자체를 차단 | 버스(데이터 통로) | 적음 — 계산 자체가 시작 안 됨 |
| ALU 내부 (덧셈 vs 뺄셈 등) | 이미 나온 결과 중 선택 | 멀티플렉서 | 있음 — 나머지도 이미 계산은 끝나 있었음 |
왜 이렇게 "낭비"를 감수하면서까지 세부 회로를 항상 다 계산해두는가 하면, 속도 때문이다. 만약 디코더가 먼저 판단을 끝낸 뒤에야 해당 회로가 계산을 시작한다면 "판단 시간 + 계산 시간"을 순차적으로 기다려야 한다. 반대로 모든 세부 회로가 미리 병렬로 계산해두면, 디코더의 판단이 끝나는 순간 이미 나와 있는 결과를 통과시키기만 하면 되므로 전체 지연 시간이 훨씬 짧아진다. ALU 내부의 세부 회로는 크기가 작아 이 정도 낭비는 감수할 만하지만, ALU와 FPU처럼 장치 자체가 큰 단위에서는 처음부터 입력을 걸러내는 쪽이 이득이라, 두 선택 단계의 설계 방식이 다르다.
마무리 — 관통하는 하나의 원칙
정수·실수·문자는 결국 다 0과 1이지만, 다음과 같은 층위로 나눠서 봐야 전체 그림이 맞아떨어진다.
- 하드웨어(CPU, 메모리): 특정 비트 패턴(opcode)이 들어오면 미리 배선된 회로가 반응하는, 의미를 모르는 물리 장치다.
- 프로그래밍 언어: 이 하드웨어를 조작하기 위한 지시 체계다. print(), for문 같은 편의 문법도 결국 컴파일러를 통해 LOAD/STORE/OPERATE/JUMP로 환원된다.
- 타입: "이 비트뭉치로 어떤 회로(ALU/FPU)를 켤지"를 결정하는 필수 정보다.
- opcode의 실제 작동: 디코더는 opcode를 읽고 여러 제어 신호를 동시에 만들어낸다. 이 신호들은 버스에서는 "필요 없는 장치로 데이터가 못 가게 막는" 역할을, 장치 내부(멀티플렉서)에서는 "이미 병렬로 계산된 결과 중 하나만 통과시키는" 역할을 한다. 계산 자체는 opcode의 판단을 기다리지 않고 항상 동시에 진행되고 있다.
- 컴파일 시점: 컴파일러가 소스 코드를 전체 맥락과 함께 읽고, 심볼 테이블에 타입을 기록하고, 그 타입에 맞는 opcode를 골라 기계어에 박아 넣는 유일한 순간이다. 타입을 "아는" 지적 작업은 여기서 끝난다.
- 런타임: CPU가 이미 결정된 opcode를 그저 순서대로 재생하는 시간이다. 타입을 다시 판단하지 않는다.
결국 컴퓨터는 어디에서도 스스로 "의미"를 판단하지 않는다. 회로 설계자가 opcode-회로 매핑을 물리적으로 배선하고, 표준화 기구가 인코딩 규칙을 정하고, 컴파일러가 타입 규칙에 따라 명령어를 선택하고, 프로그래머가 타입을 선언하거나(일반 변수) 형식 지정자를 직접 명시한다(printf 같은 예외). 컴퓨터는 이 모든 "인간이 내린 약속들"을, 각자의 층위(하드웨어 배선, 컴파일 시점의 번역, 런타임의 실행)에서 그저 정해진 대로 반복 수행할 뿐이다.