MySQL과 PostgreSQL은 모두 유명한 오픈소스 데이터베이스입니다.
그런데 서로 내부의 아키텍처는 다릅니다. 왜 일까요?
MySQL: 스레드 기반 (Thread-based)
PostgreSQL: 프로세스 기반 (Process-based)
이것을 이해하기 위해서는 프로세스와 스레드의 개념이 필요합니다.
A. 프로세스와 스레드의 기본
Concept
Process
- 독립적인 프로그램 실행 환경
특징
- 각 프로세스는 완전히 독립된 메모리 공간을 가짐
- 다른 프로세스의 메모리에 접근할 수 없음 (OS가 막음)
- 안전하지만, 생성 비용이 비쌈
┌─────────────────────┐
│ Process A (PID 100) │
├─────────────────────┤
│ Code (실행 코드) │
│ Data (전역변수) │
│ Heap (동적 할당) │
│ Stack (함수 호출) │
├─────────────────────┤
│ Page Table │ ← 자신만의 메모리 매핑
│ PCB │ ← 자신의 제어 정보
└─────────────────────┘
┌─────────────────────┐
│ Process B (PID 101) │
├─────────────────────┤
│ Code (다른 코드) │
│ Data (다른 데이터) │
│ Heap │
│ Stack │
├─────────────────────┤
│ Page Table │ ← 완전히 다른 매핑
│ PCB │
└─────────────────────┘
Thread
- 같은 프로세스 내의 여러 “실행 흐름”
특징:
- 같은 프로세스 내의 스레드들은 Code, Data, Heap을 공유
- 각 스레드는 자신의 Stack과 TCB만 독립적으로 가짐
- 빠르지만, 메모리 공유로 인한 복잡성
┌────────────────────────────────────┐
│ Process A (PID 100) │
├────────────────────────────────────┤
│ Code (실행 코드) - 공유 │
│ Data (전역변수) - 공유 │
│ Heap (동적 할당) - 공유 │
├────────────────────────────────────┤
│ Thread 1 │ Thread 2 │
│ ┌─────────────┐ │ ┌─────────────┐│
│ │Stack 1 │ │ │Stack 2 ││
│ │TCB 1 │ │ │TCB 2 ││
│ │(독립적) │ │ │(독립적) ││
│ └─────────────┘ │ └─────────────┘│
└────────────────────────────────────┘
생성 비용 비교 : Process vs Thread
Process 생성 절차
1. PCB (Process Control Block) 생성
└─ 프로세스의 정보를 담을 구조체 생성
2. 독립된 메모리 공간 할당
├─ Code 영역: 프로그램 코드 로드
├─ Data 영역: 초기화된 전역변수 준비
├─ Heap: 동적 할당을 위한 준비
└─ Stack: 함수 호출을 위한 준비
3. Page Table 구축
└─ 가상 주소 ↔ 물리 주소 매핑 만들기
(매우 복잡! 모든 메모리 영역의 매핑이 필요)
4. 다양한 커널 자료구조 초기화
├─ 파일 디스크립터 테이블
├─ 신호 처리 정보
└─ 기타 OS 자원들
총 비용:
- 메모리: 프로세스 1개 = 5~20 MB
- 시간: 생성에 수십~수백 ms 소요
- 예시: 프로세스 1000개 = 5~20 GB 메모리, 수초의 초기화 시간
Thread 생성 절차
1. TCB (Thread Control Block) 생성
└─ 스레드의 정보를 담을 작은 구조체 생성
2. 독립된 Stack만 할당
└─ 대략 1~2 MB 정도만 필요
└─ Code, Data, Heap은 이미 있으니 "포인터"로 공유
3. 레지스터 상태 초기화
└─ 프로그램 카운터, 스택 포인터 등만 설정
4. OS 자료구조는 기존 프로세스 것 재사용
총 비용:
- 메모리: 스레드 1개 = 약 1 MB (Stack만)
- 시간: 생성에 마이크로초 단위
- 예시: 스레드 1000개 = 1 GB 메모리, 밀리초 단위 초기화
결론
- 스레드가 프로세스보다 100~1000배 빠르고 가볍다!
Context Switching
가정
single coreCPU를 사용한다고 가정.
상황
- 여러 작업을 동시에 수행해야 함.
명제
- CPU는 한 번에 한 개의 프로세스/스레드만 실행할 수 있음.
이를 위해서는 CPU 가 쉴 틈 없이 많은 작업을 하게 만들어서 다양한 일을 동시에 처리하는 것처럼 사용자가 느끼게 해야함.
→ Context Switching
Time 0-100ms: 프로세스 A 실행
Time 100-200ms: 프로세스 B 실행
Time 200-300ms: 프로세스 A 실행
...
그러나 이 Context Switching 비용 역시 Process 와 Thread 에서 차이가 존재한다.
프로세스 간 Context Switching
- TLB 재구축: ~1000 CPU 사이클
- 캐시 재구축: ~10마이크로초 ~ 1밀리초
- 총 비용: 1~10 ms 정도 (매우 느림!)
프로세스 A 실행 중...
↓
[Context Switch 발생]
↓
OS의 할 일:
1. 프로세스 A의 CPU 레지스터 값 저장
2. 프로세스 B의 CPU 레지스터 값 복원
3. ⚠️ MMU(Memory Management Unit) 설정 변경
├─ Page Table을 A의 것에서 B의 것으로 전환
└─ 가상 주소 → 물리 주소 매핑 완전히 바뀜
4. ⚠️ TLB (Translation Lookaside Buffer) 비우기
└─ CPU의 주소 변환 캐시를 완전히 초기화
└─ 이유: A의 매핑이 캐시되어 있는데 B가 필요함
5. ⚠️ CPU 캐시 오염
└─ L1/L2 캐시에 A의 데이터가 남아있음
└─ B가 처음부터 새로 캐시를 채워야 함
↓
프로세스 B 실행 시작 (But 초기에는 매우 느림)스레드 간 Context Switching
- 레지스터만 교환: ~100 CPU 사이클
- 총 비용: 약 1 마이크로초 (빠름!)
스레드 A 실행 중...
↓
[Context Switch 발생]
↓
OS의 할 일:
1. 스레드 A의 CPU 레지스터 값 저장
2. 스레드 B의 CPU 레지스터 값 복원
3. ✓ MMU 설정 변경 안 함!
└─ 같은 프로세스이므로 Page Table 그대로
4. ✓ TLB 비우기 안 함!
└─ Page Table이 같으므로 기존 매핑 유효
5. ✓ CPU 캐시도 대부분 유효
└─ A와 B가 같은 메모리에 접근할 확률이 높음
↓
스레드 B 실행 시작 (거의 지연 없음)결론
- 스레드 전환이 프로세스 전환보다 약 1000배 빠르다!
B. 왜 MySQL은 스레드를 선택했을까?
MySQL 를 웹서비스와 함께 사용하는 상황을 가정해보자. 그리고 이 서비스는 나름 인기를 얻고 있어서 동시에 1000명의 사용자가 접속할 정도라면 아래와 같은 구조가 된다.
웹 서버 + MySQL 구조:
Client 1 ─┐
Client 2 ─┤
Client 3 ─┼─→ MySQL 서버
... │
Client 1000─┘
동시에 1000명의 클라이언트가 MySQL에 접근
이제 Process 기반이었을 경우와, Thread 기반을 비교해서 살펴보자.
Connection Pool 같은 기술은 사용하지 않는 경우를 가정했다.
Process
Client 1 ──→ Process 1 (5 MB)
Client 2 ──→ Process 2 (5 MB)
Client 3 ──→ Process 3 (5 MB)
...
Client 1000 ──→ Process 1000 (5 MB)
- 메모리 사용량 多
- 하나의
Process가 사용하는 메모리가5MB라고 하면, 1000명의 사용자를 위해5GB의 메모리를 사용.
- 하나의
- Context Switching 비용 多
- Context Switching에 많은
cpu자원이 낭비됨.
- Context Switching에 많은
├─ 프로세스 1 → 프로세스 2: 1~10 ms 지연
├─ 프로세스 2 → 프로세스 3: 1~10 ms 지연
Thread
- 비교적 적은 메모리 사용량
- 하나의
Thread가 사용하는 메모리가 프로세스의 20% 정도인1MB라고 하자. 그 경우, 총1GBThread가 더 적은 메모리를 사용하는 이유를 공감하지 못한다면, 위를 다시 보자.
- 하나의
- 비교적 적은
Context Switching시간 - 추가 이점
- Buffer Pool (캐시) 공유: 모든 스레드가 함께 사용
- 데이터 접근 속도 극도로 빠름
Client 1 ──→ Thread 1 (1 MB)
Client 2 ──→ Thread 2 (1 MB)
Client 3 ──→ Thread 3 (1 MB)
...
Client 1000 ──→ Thread 1000 (1 MB)
Context Switching도 빠름:
├─ Thread 1 → Thread 2: ~1 마이크로초
├─ Thread 2 → Thread 3: ~1 마이크로초
└─ 거의 무시할 수 있는 수준!
결론
Thread기반 구조를 사용할 경우, 메모리 절감 + 속도 향상를 통해 더 많은 동시 사용자 처리가 가능하여MySQL은Thread기반 구조를 채택
C. 스레드 기반의 문제
그러나 Thread 를 사용하기 때문에 Process 기반에서는 발생하지 않을 문제도 보유하게 됨.
메모리 공유로 인한 데이터 정합성 문제- 하나의 스레드의 오류의 전파
C-1: 메모리 공유로 인한 데이터 오염
- 상황
- 여러
Thread가 같은 변수에 접근해서 값을1만큼 올리는 동작을 하는 것을 상상해보자.
- 여러
Thread 1 Thread 2
↓ ↓
└──→ [공유 메모리 변수] ←──┘
count = 0
Thread 1에서: count = count + 1
Thread 2에서: count = count + 1
문제 상황 → Race Condition
기대값: count = 2
실제값: count = 1 (또는 다른 값!) ❌
발생 원인
- 실행 순서의 보장 X.
Time 1: Thread 1: count 읽음 (0)
Time 2: Thread 2: count 읽음 (0) ← 아직 Thread 1의 변경이 반영 안 됨
Time 3: Thread 1: count + 1 = 1 쓰기
Time 4: Thread 2: count + 1 = 1 쓰기 ← 결과: 1
C-2: 하나의 스레드 오류가 전체를 죽임
- 하나의
thread오류가 서비스 전체에 영향을 끼칠 수 있음
모든 클라이언트 ──→ MySQL 서버 (하나의 프로세스) ──→ Thread 1, 2, 3...
Thread 2가 메모리 오류 (Segmentation Fault) 발생!
↓
프로세스 전체가 죽음 ❌
↓
모든 스레드가 중단 ❌
↓
모든 클라이언트 연결 끊김 😱
반면 Process 기반에서는 이러한 문제가 발생하지 않음.
프로세스 기반 (PostgreSQL):
Client 1 ──→ Process 1
Client 2 ──→ Process 2 ← 이 프로세스가 오류 발생!
Client 3 ──→ Process 3
Process 2가 죽어도:
├─ Process 1은 계속 실행 ✓
├─ Process 3도 계속 실행 ✓
└─ Client 2만 연결 끊김
C-3. MySQL에서 Thread의 문제를 해결한 방법
해결책 1: MVCC (Multi-Version Concurrency Control)
아이디어: 데이터를 “버전 관리”처럼 처리
원본 데이터: name = "Alice", age = 25
Thread 1: UPDATE로 age를 30으로 변경하려고 함
MVCC가 하는 일:
├─ 원본 보존: Version 0 (age = 25) - 계속 유지
├─ 새 버전 생성: Version 1 (age = 30) - Thread 1이 수정 중
└─ Undo Log: 이전 버전 기록
동시에 다른 스레드들:
├─ Thread 2: SELECT name, age → Version 0 읽음 (age = 25)
├─ Thread 3: SELECT name, age → Version 0 읽음 (age = 25)
└─ Thread 1: UPDATE 완료 → Version 1 완성 (age = 30)
결과: 잠금 없이도 안전한 읽기 가능!
효과: Race Condition 방지, 동시성 증가
해결책 2: Fine-Grained Locking (세분화된 잠금)
아이디어: 전체 데이터베이스를 잠그지 말고, 필요한 부분만 잠금
❌ 과거 방식 (느림):
Buffer Pool 전체를 하나의 Lock으로 관리
↓
Thread 1이 Lock 획득 → 페이지 접근
Thread 2가 대기... → 다른 페이지에 접근하고 싶어도 못함
Thread 3도 대기...
결과: 많은 대기 시간
✅ 현대 MySQL:
Buffer Pool을 여러 Instance로 분할
├─ Instance 1 [Lock 1] → Thread 1이 사용
├─ Instance 2 [Lock 2] → Thread 2가 동시에 사용 ✓
├─ Instance 3 [Lock 3] → Thread 3도 동시에 사용 ✓
└─ Instance 4 [Lock 4] → Thread 4도 동시에 사용 ✓
결과: 대기 시간 극감!
효과: 스레드가 많아도 전체가 느려지지 않음
해결책 3: Signal Handling (신호 처리)
아이디어: 스레드 오류를 감지하고 격리
Thread 2에서 메모리 오류 발생!
↓
MySQL의 Signal Handler가 감지
↓
Thread 2가 가진 Lock/Mutex를 강제로 해제
↓
Thread 2만 종료 (다른 스레드는 계속 실행) ✓
↓
(하지만 완벽한 해결은 아님 - 메모리가 이미 오염되었을 수 있음)
효과: 일부 상황에서는 전체 시스템 붕괴 방지
7️⃣ PostgreSQL은 왜 프로세스를 고수했을까?
안정성의 철학
PostgreSQL의 생각:
"우리는 금융, 의료, 정부 시스템처럼
데이터 손실이 절대 안 되는 분야도 사용할 것이다.
따라서 스레드의 복잡성보다
프로세스의 강력한 격리가 더 중요하다."
각 클라이언트 ──→ 독립 프로세스 (물리적 격리)
↓
한 프로세스의 오류가 다른 프로세스에 영향 없음
↓
데이터 무결성 보장!
PostgreSQL이 감수하는 비용
대신에...
1. 메모리 사용량이 많음
└─ Client 1000명 = 5~20 GB 메모리 필요
2. Context Switching 오버헤드 많음
└─ 1000개 프로세스 간 전환 = 시간 낭비
3. 하지만 안정성은 최고
└─ 한 클라이언트의 오류가 다른 클라이언트에 영향 없음
8️⃣ MySQL이 PostgreSQL에게 지는 상황
시나리오 A: 복잡한 분석 쿼리 (OLAP)
쿼리: 100억 건의 데이터를 분석하고 집계
MySQL의 한계:
├─ 하나의 쿼리는 기본적으로 1개 스레드만 사용
├─ 32 코어 CPU가 있어도 1개 코어만 사용
└─ 처리 시간: 100초
PostgreSQL의 병렬 처리:
├─ 하나의 쿼리를 여러 프로세스로 분산
├─ 32 코어 CPU를 거의 모두 활용
└─ 처리 시간: 5초 (20배 빠름!)
결론: OLAP은 PostgreSQL이 훨씬 유리
시나리오 B: 장시간 실행 쿼리
상황: 읽기 쿼리 A가 1시간 이상 실행 중
MySQL (MVCC 문제):
├─ 쿼리 A가 오래된 버전을 참조하고 있음
├─ 그 동안 UPDATE가 1000만 건 발생
├─ Undo Log 크기: 점점 증가 (메모리 낭비)
├─ 디스크 I/O 폭증 (성능 저하)
└─ Purge Thread 과부하 (전체 시스템 느려짐)
PostgreSQL:
├─ VACUUM 작업이 명시적이고 예측 가능
├─ 필요할 때만 Lock을 잠시 획득하고 정리
└─ 성능이 더 예측 가능
9️⃣ 정리: 언제 뭘 써야 할까?
MySQL 사용하기 좋은 상황
✅ OLTP (짧은 쿼리 폭주)
예: 웹 서비스 (SNS, 전자상거래)
- 각 쿼리 실행 시간: 100ms 미만
- 동시 접속자: 수천~수만
- 빠른 응답이 가장 중요
→ MySQL 최적
✅ 메모리가 제한적인 경우
예: 클라우드 환경 (작은 VM)
- 사용 가능 메모리: 4GB
- 동시 사용자: 수백~수천
→ 프로세스 기반은 메모리 초과
→ MySQL의 스레드 기반이 필수
PostgreSQL 사용하기 좋은 상황
✅ OLAP (복잡한 분석)
예: 데이터 웨어하우스
- 쿼리 실행 시간: 수분~수시간
- 병렬 처리가 중요
- 데이터 정합성 중요
→ PostgreSQL 최적
✅ 안정성이 생명인 경우
예: 금융, 의료, 정부 시스템
- 데이터 손실 절대 불가
- 버그로 인한 시스템 다운 절대 불가
- 강력한 격리가 필수
→ PostgreSQL의 프로세스 기반 격리가 필수
✅ 데이터가 매우 큰 경우
예: 테라바이트 단위 데이터 분석
- 메모리 사용량이 중요하지 않음
- 정확한 데이터 처리가 가장 중요
→ PostgreSQL
🔟 결론: 트레이드오프 (Trade-off)
MySQL은 우월한 것이 아니라 "최적화된 것"일 뿐입니다.
MySQL의 선택:
"성능을 극대화하되,
안정성의 마지노선을 소프트웨어(MVCC, Fine-grained Lock)로 관리한다"
PostgreSQL의 선택:
"성능은 조금 포기하되,
OS의 프로세스 격리로 절대적인 안정성을 보장한다"
둘 다 합리적인 선택입니다.
상황에 따라 최적의 도구가 다를 뿐입니다.
1️⃣1️⃣ 학부 시험 대비 핵심 정리
Q1: MySQL이 스레드를 선택한 이유는?
답:
1. 메모리 효율: 스레드는 프로세스보다 메모리 사용 5배 적음
2. Context Switching: 스레드 전환이 프로세스 전환보다 1000배 빠름
3. 캐시 효율: 같은 주소 공간을 사용하므로 캐시 히트율 높음
4. 많은 동시 연결 처리 가능
Q2: PostgreSQL이 프로세스를 고수한 이유는?
답:
1. 격리: 한 클라이언트 오류가 다른 클라이언트에 영향 없음
2. 안정성: OS 레벨에서 메모리 보호
3. 병렬 처리: 하나의 쿼리를 여러 프로세스로 분산 가능
4. 금융/의료 같은 안전성 중요 분야에 적합
Q3: MySQL의 위험성은?
답:
한 스레드의 오류 → 전체 프로세스 죽음 → 모든 클라이언트 연결 끊김
해결책:
1. MVCC: 버전 관리로 Race Condition 방지
2. Fine-grained Locking: 세분화된 잠금으로 경합 감소
3. Signal Handling: 오류 감지 및 격리 시도
(하지만 완벽하지는 않음)
Q4: Context Switching이 왜 느릴까?
답:
프로세스 전환:
- Page Table 변경 필요 (가상 주소 매핑이 완전히 다름)
- TLB (주소 변환 캐시) 초기화 필요
- CPU 캐시도 초기화됨
- 총 1~10ms 소요
스레드 전환:
- Page Table 변경 불필요 (같은 주소 공간)
- TLB 유지 가능
- CPU 캐시도 대부분 유효
- 총 1마이크로초 소요
→ 약 1000배의 차이!
1️⃣2️⃣ 추가 학습: 실무 관점
MySQL을 쓸 때 조심할 점
1. 장시간 실행 쿼리 금지
└─ Purge Lag 발생 → 전체 시스템 느려짐
2. 한 번에 너무 많은 UPDATE 금지
└─ 메모리 폭증 → Undo Log 비대화
3. 복잡한 분석은 피하기
└─ 병렬 처리 불가 → 시간 초과
4. 연결 풀 사용 필수
└─ 스레드 생성 오버헤드 줄이기
PostgreSQL을 쓸 때 장점
1. 복잡한 분석도 안심
└─ 병렬 처리가 자동으로 최적화
2. 메모리 오염 걱정 없음
└─ 프로세스가 물리적으로 격리됨
3. 데이터 무결성 최고
└─ 많은 금융권에서 선택
마지막: 기억해야 할 것
“기술에 정답은 없습니다. 상황에 따라 트레이드오프를 이해하고 최적의 선택을 하는 것이 엔지니어의 역량입니다.”
MySQL과 PostgreSQL의 차이는:
- 어느 것이 우월한가의 문제가 아니라
- 어떤 상황에 최적화되어 있는가의 문제입니다.