[CS 기초 #2] 컴퓨터 구조: CPU, 메모리, 저장장치의 동작 원리 완벽 이해
컴퓨터 속을 들여다봅시다! CPU, 메모리, 저장장치가 어떻게 협력하여 여러분의 코드를 실행하는지 알면 성능 최적화의 길이 보입니다.
🎯 이 글을 읽고 나면
- 폰 노이만 구조와 현대 컴퓨터의 기본 설계를 이해합니다
- CPU의 Fetch-Decode-Execute 사이클을 설명할 수 있습니다
- 메모리 계층구조와 캐시의 동작 원리를 알게 됩니다
- HDD와 SSD의 차이점과 성능 특성을 비교할 수 있습니다
- 캐시 친화적인 코드를 작성할 수 있습니다
📚 사전 지식
- 기본 프로그래밍 경험: 변수, 배열 등 기본 개념 이해
- CS 기초 개념: 이전 글: CS 입문을 읽으면 더 좋습니다
- 이진수는 다음 글에서! 지금은 몰라도 괜찮습니다
💡 핵심 개념 미리보기
컴퓨터는 CPU(연산), 메모리(임시 저장), 저장장치(영구 저장)라는 세 가지 핵심 구성요소로 이루어져 있습니다. CPU는 메모리에서 명령어를 가져와(Fetch) 해석하고(Decode) 실행(Execute)하는 사이클을 반복합니다. 메모리는 속도와 용량의 트레이드오프를 고려한 계층 구조로 설계되어 있으며, 이를 이해하면 더 빠른 코드를 작성할 수 있습니다!
🏗️ 들어가며: 컴퓨터는 어떻게 동작하는가?
우리가 작성한 코드 한 줄이 실제로 어떻게 실행되는지 궁금하신가요? 오늘은 컴퓨터의 핵심 구성요소인 CPU, 메모리, 저장장치가 어떻게 동작하고 서로 협력하는지 알아보겠습니다.
이 지식은 단순한 이론이 아닙니다. 성능 최적화, 메모리 관리, 시스템 설계에 직접적으로 활용됩니다!
📐 폰 노이만 구조: 현대 컴퓨터의 기본 설계
1945년 폰 노이만이 제안한 이 구조는 현재까지도 대부분의 컴퓨터가 따르는 기본 설계입니다.
graph TB
subgraph "폰 노이만 구조"
CPU[CPU<br/>중앙처리장치]
Memory[Memory<br/>주기억장치]
Input[Input<br/>입력장치]
Output[Output<br/>출력장치]
CPU <--> Memory
Input --> CPU
CPU --> Output
end
subgraph "CPU 내부"
CU[Control Unit<br/>제어장치]
ALU[ALU<br/>산술논리장치]
REG[Registers<br/>레지스터]
CU --> ALU
CU --> REG
ALU <--> REG
end
핵심 특징
- 프로그램 내장 방식: 프로그램과 데이터를 같은 메모리에 저장
- 순차적 실행: 명령어를 하나씩 순서대로 실행
- 이진 시스템: 모든 정보를 0과 1로 표현
🔰 초보자를 위한 비유
컴퓨터를 공장에 비유해봅시다!
- CPU (중앙처리장치): 공장의 작업자. 설계도(프로그램)를 보고 실제로 일을 합니다
- 메모리 (RAM): 작업대. 지금 당장 사용할 재료와 도구를 올려놓습니다 (전원이 꺼지면 사라짐!)
- 저장장치 (HDD/SSD): 창고. 모든 자료를 영구적으로 보관합니다
- 캐시: 손이 닿는 곳의 소형 작업대. 자주 쓰는 것만 올려놓아 빠르게 접근
작업자(CPU)가 창고(저장장치)까지 가려면 시간이 오래 걸리지만, 작업대(메모리)나 손 닿는 곳(캐시)에 있으면 금방 가져올 수 있습니다!
🧠 CPU (Central Processing Unit)
CPU는 컴퓨터의 두뇌입니다. 모든 연산과 제어를 담당합니다.
CPU의 구성 요소
graph LR
subgraph "CPU 구조"
subgraph "제어 장치"
PC[Program Counter]
IR[Instruction Register]
Decoder[명령어 해석기]
end
subgraph "산술논리장치"
Adder[덧셈기]
Logic[논리 연산기]
Comparator[비교기]
end
subgraph "레지스터"
GPR[범용 레지스터]
SP[스택 포인터]
ACC[누산기]
end
subgraph "캐시"
L1[L1 캐시<br/>32KB]
L2[L2 캐시<br/>256KB]
L3[L3 캐시<br/>8MB]
end
end
CPU 명령어 실행 사이클 (Fetch-Decode-Execute)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# CPU 동작을 시뮬레이션하는 간단한 예제
class SimpleCPU:
def __init__(self):
self.pc = 0 # Program Counter
self.registers = [0] * 8 # 8개의 레지스터
self.memory = [0] * 256 # 256바이트 메모리
def fetch(self):
"""메모리에서 명령어를 가져옴"""
instruction = self.memory[self.pc]
self.pc += 1
return instruction
def decode(self, instruction):
"""명령어를 해석"""
opcode = instruction >> 4 # 상위 4비트: 명령 코드
operand = instruction & 0xF # 하위 4비트: 피연산자
return opcode, operand
def execute(self, opcode, operand):
"""명령어를 실행"""
if opcode == 0x1: # LOAD
self.registers[0] = self.memory[operand]
elif opcode == 0x2: # ADD
self.registers[0] += self.registers[operand]
elif opcode == 0x3: # STORE
self.memory[operand] = self.registers[0]
# ... 더 많은 명령어들
def run_cycle(self):
"""한 사이클 실행"""
instruction = self.fetch()
opcode, operand = self.decode(instruction)
self.execute(opcode, operand)
# 사용 예시
cpu = SimpleCPU()
# 메모리에 프로그램 로드
cpu.memory[0] = 0x10 # LOAD 0 (메모리 0번지 값을 레지스터에 로드)
cpu.memory[1] = 0x21 # ADD 1 (레지스터 1번과 더하기)
cpu.memory[2] = 0x30 # STORE 0 (결과를 메모리 0번지에 저장)
현대 CPU의 고급 기능
1. 파이프라이닝 (Pipelining)
여러 명령어를 동시에 처리하여 성능 향상
1
2
3
4
시간 →
명령어1: [Fetch][Decode][Execute][Memory][WriteBack]
명령어2: [Fetch][Decode][Execute][Memory][WriteBack]
명령어3: [Fetch][Decode][Execute][Memory][WriteBack]
2. 분기 예측 (Branch Prediction)
조건문의 결과를 미리 예측하여 파이프라인 효율성 증대
3. 멀티코어 (Multi-core)
여러 개의 CPU 코어로 병렬 처리
💾 메모리 (Memory)
메모리는 데이터와 프로그램을 임시로 저장하는 공간입니다.
메모리 계층 구조
graph TD
subgraph "메모리 계층구조"
REG[레지스터<br/>1ns, 수십 바이트]
L1[L1 캐시<br/>1-2ns, 32KB]
L2[L2 캐시<br/>3-10ns, 256KB]
L3[L3 캐시<br/>10-20ns, 8MB]
RAM[주 메모리 RAM<br/>50-100ns, 16GB]
SSD[SSD<br/>10-100μs, 512GB]
HDD[HDD<br/>5-10ms, 2TB]
REG --> L1
L1 --> L2
L2 --> L3
L3 --> RAM
RAM --> SSD
SSD --> HDD
end
style REG fill:#ff9999
style L1 fill:#ffcc99
style L2 fill:#ffff99
style L3 fill:#ccff99
style RAM fill:#99ffcc
style SSD fill:#99ccff
style HDD fill:#cc99ff
메모리 접근 속도와 용량의 트레이드오프
| 메모리 종류 | 접근 시간 | 용량 | 가격/GB |
|---|---|---|---|
| 레지스터 | 1ns | 수십 바이트 | - |
| L1 캐시 | 1-2ns | 32KB | $$$ |
| L2 캐시 | 3-10ns | 256KB | $$ |
| L3 캐시 | 10-20ns | 8MB | $ |
| RAM | 50-100ns | 16GB | $ |
| SSD | 10-100μs | 512GB | ¢ |
| HDD | 5-10ms | 2TB | ¢ |
캐시의 동작 원리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class CacheSimulator:
def __init__(self, cache_size=4):
self.cache = {} # 캐시 메모리
self.cache_size = cache_size
self.hits = 0
self.misses = 0
self.access_order = [] # LRU를 위한 접근 순서
def read(self, address):
"""메모리 읽기 시뮬레이션"""
if address in self.cache:
# 캐시 히트
self.hits += 1
# LRU 업데이트
self.access_order.remove(address)
self.access_order.append(address)
return self.cache[address], "HIT"
else:
# 캐시 미스
self.misses += 1
# 메모리에서 데이터 로드 (시뮬레이션)
data = f"Data at {address}"
# 캐시가 가득 찬 경우 LRU 제거
if len(self.cache) >= self.cache_size:
lru_address = self.access_order.pop(0)
del self.cache[lru_address]
# 새 데이터를 캐시에 추가
self.cache[address] = data
self.access_order.append(address)
return data, "MISS"
def get_hit_rate(self):
total = self.hits + self.misses
return (self.hits / total * 100) if total > 0 else 0
# 캐시 동작 테스트
cache = CacheSimulator(cache_size=3)
addresses = [1, 2, 3, 1, 4, 1, 2, 5, 1]
for addr in addresses:
data, status = cache.read(addr)
print(f"주소 {addr}: {status}")
print(f"\n캐시 적중률: {cache.get_hit_rate():.1f}%")
가상 메모리 (Virtual Memory)
물리적 RAM보다 큰 프로그램을 실행할 수 있게 해주는 기술입니다.
graph LR
subgraph "가상 메모리"
VP1[가상 페이지 1]
VP2[가상 페이지 2]
VP3[가상 페이지 3]
VP4[가상 페이지 4]
end
subgraph "물리 메모리"
PP1[물리 페이지 1]
PP2[물리 페이지 2]
PP3[물리 페이지 3]
end
subgraph "디스크"
DP1[스왑 영역]
end
VP1 --> PP1
VP2 --> PP3
VP3 --> PP2
VP4 --> DP1
🗄️ 저장장치 (Storage)
저장장치는 데이터를 영구적으로 보관하는 장치입니다.
HDD vs SSD 비교
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 저장장치 성능 비교 시뮬레이션
import time
import random
class StorageDevice:
def __init__(self, name, seek_time, transfer_rate):
self.name = name
self.seek_time = seek_time # ms
self.transfer_rate = transfer_rate # MB/s
def read_sequential(self, size_mb):
"""순차 읽기 시간 계산"""
return self.seek_time + (size_mb / self.transfer_rate * 1000)
def read_random(self, count, size_kb):
"""랜덤 읽기 시간 계산"""
total_time = 0
for _ in range(count):
total_time += self.seek_time
total_time += (size_kb / 1024 / self.transfer_rate * 1000)
return total_time
# 저장장치 정의
hdd = StorageDevice("HDD", seek_time=10, transfer_rate=150)
ssd = StorageDevice("SSD", seek_time=0.1, transfer_rate=550)
# 성능 비교
print("100MB 순차 읽기:")
print(f"HDD: {hdd.read_sequential(100):.1f}ms")
print(f"SSD: {ssd.read_sequential(100):.1f}ms")
print("\n1000개의 4KB 랜덤 읽기:")
print(f"HDD: {hdd.read_random(1000, 4):.1f}ms")
print(f"SSD: {ssd.read_random(1000, 4):.1f}ms")
RAID (Redundant Array of Independent Disks)
여러 디스크를 조합하여 성능과 안정성을 향상시키는 기술
graph TB
subgraph "RAID 0 - Striping"
D1A[디스크1<br/>A C E]
D2A[디스크2<br/>B D F]
end
subgraph "RAID 1 - Mirroring"
D1B[디스크1<br/>A B C]
D2B[디스크2<br/>A B C]
end
subgraph "RAID 5 - Distributed Parity"
D1C[디스크1<br/>A D P]
D2C[디스크2<br/>B P E]
D3C[디스크3<br/>P C F]
end
🔄 시스템 버스: 구성요소들의 고속도로
graph LR
subgraph "시스템 버스"
CPU[CPU]
Memory[메모리]
IO[I/O 장치]
CPU <--> DataBus[데이터 버스<br/>데이터 전송]
CPU <--> AddressBus[주소 버스<br/>위치 지정]
CPU <--> ControlBus[제어 버스<br/>명령 전달]
DataBus <--> Memory
AddressBus --> Memory
ControlBus --> Memory
DataBus <--> IO
AddressBus --> IO
ControlBus --> IO
end
💡 실무에서의 활용
1. 성능 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 캐시 친화적인 코드 vs 캐시 비친화적인 코드
# 나쁜 예: 캐시 미스가 많이 발생
def matrix_sum_bad(matrix):
total = 0
rows = len(matrix)
cols = len(matrix[0])
# 열 우선 순회 - 캐시 비효율적
for j in range(cols):
for i in range(rows):
total += matrix[i][j]
return total
# 좋은 예: 캐시 히트율이 높음
def matrix_sum_good(matrix):
total = 0
# 행 우선 순회 - 캐시 효율적
for row in matrix:
for value in row:
total += value
return total
# 메모리는 행 단위로 연속 저장되므로
# 행 우선 순회가 캐시 지역성을 활용
2. 메모리 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 메모리 풀링으로 할당/해제 오버헤드 감소
class MemoryPool:
def __init__(self, object_class, pool_size=100):
self.object_class = object_class
self.pool = [object_class() for _ in range(pool_size)]
self.available = list(self.pool)
self.in_use = []
def acquire(self):
if self.available:
obj = self.available.pop()
self.in_use.append(obj)
return obj
else:
# 풀이 비어있으면 새로 생성
obj = self.object_class()
self.in_use.append(obj)
return obj
def release(self, obj):
if obj in self.in_use:
self.in_use.remove(obj)
self.available.append(obj)
# 객체 초기화
obj.reset()
3. 병렬 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import multiprocessing
def cpu_bound_task(n):
"""CPU 집약적 작업"""
total = 0
for i in range(n):
total += i * i
return total
# 멀티코어 활용
def parallel_processing():
# CPU 코어 수 확인
num_cores = multiprocessing.cpu_count()
print(f"사용 가능한 CPU 코어: {num_cores}")
# 작업을 코어 수만큼 분할
with multiprocessing.Pool(num_cores) as pool:
tasks = [10000000] * num_cores
results = pool.map(cpu_bound_task, tasks)
return sum(results)
🎯 핵심 정리
기억해야 할 5가지
- CPU는 Fetch-Decode-Execute 사이클로 동작
- 메모리 계층구조는 속도와 용량의 트레이드오프
- 캐시는 자주 사용하는 데이터를 빠르게 접근 가능하게 함
- 가상 메모리로 물리 메모리보다 큰 프로그램 실행 가능
- 병렬 처리로 멀티코어 CPU 성능 활용
성능 개선 팁
- 데이터 지역성을 고려한 코드 작성
- 캐시 친화적인 알고리즘 선택
- 메모리 할당/해제 최소화
- CPU 집약적 작업은 병렬 처리
📚 더 깊이 공부하기
추천 자료
- “Computer Organization and Design” - Patterson & Hennessy
- “What Every Programmer Should Know About Memory” - Ulrich Drepper
- Intel/AMD CPU 매뉴얼 (고급)
실습 프로젝트
- 간단한 CPU 에뮬레이터 만들기
- 캐시 시뮬레이터 구현
- 메모리 관리자 작성
🚀 다음 시간 예고
다음 포스트에서는 “이진수와 논리 게이트”를 다룹니다. 컴퓨터가 0과 1만으로 어떻게 복잡한 연산을 수행하는지, 논리 게이트가 어떻게 동작하는지 알아보겠습니다!
✅ 이 글에서 배운 것
스스로 확인해보세요! 각 항목을 설명할 수 있다면 체크하세요.
개념 이해
- 폰 노이만 구조의 핵심 특징 3가지 (프로그램 내장, 순차 실행, 이진 시스템)
- CPU의 Fetch-Decode-Execute 사이클 동작 방식
- 메모리 계층구조: 레지스터 → 캐시 → RAM → SSD → HDD
- 캐시 히트(Hit)와 미스(Miss)의 개념
- 가상 메모리가 필요한 이유
실용적 이해
- 왜 행 우선 순회가 캐시 친화적인지 설명할 수 있다
- SSD가 HDD보다 랜덤 접근에서 훨씬 빠른 이유를 안다
- 멀티코어 CPU를 활용하는 방법 (병렬 처리)
- 메모리 풀링이 성능을 개선하는 원리
실습 완료
- SimpleCPU 시뮬레이터 코드를 읽고 이해했다
- CacheSimulator로 LRU 캐시 동작을 확인했다
- 캐시 친화적 코드 vs 비친화적 코드의 차이를 이해했다
- HDD와 SSD 성능 비교 코드를 실행해봤다
