MySQL을 매일 사용하면서도, 쿼리를 실행하면 내부에서 정확히 무슨 일이 벌어지는지 설명하기는 쉽지 않습니다. 파서가 뭘 하고, 옵티마이저가 뭘 결정하며, 스토리지 엔진은 왜 교체 가능한 구조인지 — 이 글에서는 클라이언트가 SQL을 보내는 순간부터 결과가 돌아오기까지의 흐름을 단계별로 살펴봅니다.
1. 전체 구조 — 두 계층의 분리
MySQL의 아키텍처는 크게 두 계층으로 나뉩니다.
- MySQL 서버 계층 (Server Layer): 쿼리 파싱, 최적화, 실행 계획 수립 등 데이터베이스의 핵심 로직을 담당합니다.
- 스토리지 엔진 계층 (Storage Engine Layer): 실제 데이터를 디스크에 읽고 쓰는 역할을 담당합니다.
이 두 계층은 명확하게 분리되어 있으며, 스토리지 엔진은 플러그인 방식으로 교체할 수 있습니다. InnoDB, MyISAM, Memory 등이 모두 이 플러그인 구조 위에서 동작합니다.

2. 스토리지 엔진 비교
실행 엔진은 스토리지 엔진과 직접 통신하지 않고, 핸들러 API(Handler API)라는 추상화 계층을 통해 통신합니다. 실행 엔진 입장에서는 아래에 InnoDB가 있든 MyISAM이 있든 동일한 인터페이스로 데이터를 요청할 수 있습니다. 이것이 MySQL이 스토리지 엔진을 플러그인처럼 교체할 수 있는 이유입니다.
| 스토리지 엔진 | 특징 |
| InnoDB | 트랜잭션, 외래키, MVCC 지원. MySQL 5.5 이후 기본 엔진 |
| MyISAM | 트랜잭션 없음. 읽기 중심 워크로드에서 단순하고 빠름 (레거시) |
| Memory | 데이터를 메모리에만 저장. 임시 테이블용 |
| CSV | CSV 파일을 테이블처럼 직접 접근 |
현대적인 MySQL 사용에서는 사실상 InnoDB가 표준입니다. MyISAM은 트랜잭션과 외래키를 지원하지 않아 데이터 무결성을 보장하기 어렵고, MySQL 8.0부터는 시스템 테이블도 모두 InnoDB로 전환되었습니다.
3. 스레드 구조
MySQL의 스레드는 역할에 따라 포그라운드 스레드와 백그라운드 스레드로 나뉩니다.
3-1. 포그라운드 스레드 (Foreground Thread)
클라이언트가 MySQL에 접속하면 커넥션 핸들러(Connection Handler)가 요청을 받고, 해당 커넥션 전용 포그라운드 스레드를 할당합니다. 인증, 권한 확인, 쿼리 처리까지 클라이언트와 직접 통신하는 모든 작업이 이 스레드에서 이루어집니다.
커넥션이 종료되면 스레드를 바로 제거하지 않고 스레드 캐시(Thread Cache)에 반환해 재사용합니다. 매번 새 스레드를 생성하고 제거하는 비용을 줄이기 위해서입니다.
커넥션 풀과 스레드 캐시의 차이
비슷해 보이지만 동작하는 위치가 다릅니다. 애플리케이션 측의 커넥션 풀(HikariCP 등)은 클라이언트가 MySQL 커넥션 자체를 재사용하기 위한 장치입니다. MySQL 서버 측의 스레드 캐시는 서버 내부에서 스레드를 재사용하기 위한 장치입니다. 둘은 서로 다른 레이어에서 독립적으로 동작합니다.
3-2. 백그라운드 스레드 (Background Thread)
포그라운드 스레드가 클라이언트 요청을 처리하는 동안, 백그라운드 스레드는 MySQL 내부의 유지 관리 작업을 담당합니다. InnoDB 기준으로 대표적인 백그라운드 스레드는 다음과 같습니다.
| 스레드 | 역할 |
| 마스터 스레드 (Master Thread) | 버퍼 풀 플러시, 언두 로그 정리 등 InnoDB 핵심 작업을 총괄 |
| 쓰기 I/O 스레드 (Write I/O Thread) | 버퍼 풀의 더티 페이지를 디스크에 비동기로 씀 |
| 로그 I/O 스레드 (Log I/O Thread) | 리두 로그 버퍼의 내용을 디스크의 리두 로그 파일에 비동기로 기록 |
| 읽기 I/O 스레드 (Read I/O Thread) | 디스크에서 데이터 페이지를 버퍼 풀로 비동기로 읽어들임 |
| 페이지 클리너 스레드 (Page Cleaner Thread) | 버퍼 풀에서 플러시할 더티 페이지를 선별해 쓰기 I/O 스레드에 전달 |
| 퍼지 스레드 (Purge Thread) | 더 이상 필요 없는 언두 로그 레코드를 정리 |
쓰기 I/O 스레드와 페이지 클리너 스레드는 역할이 나뉩니다. 페이지 클리너가 버퍼 풀을 순회하며 플러시 대상을 선별하고, 쓰기 I/O 스레드가 실제로 디스크에 기록합니다. 로그 I/O 스레드는 데이터 페이지가 아닌 리두 로그만 전담하며, 쓰기 I/O 스레드와는 독립적으로 동작합니다.
읽기/쓰기/로그 I/O 스레드는 각각 innodb_read_io_threads, innodb_write_io_threads 등의 설정으로 개수를 조정할 수 있습니다.
백그라운드 스레드의 구체적인 동작은 2부에서 InnoDB 내부 구조를 다룰 때 더 자세히 살펴봅니다.
4. 쿼리 처리 흐름
커넥션이 맺어지고 클라이언트가 SQL을 전송하면, MySQL 서버 계층은 다음 순서로 쿼리를 처리합니다.
4-1. SQL 파서 (Parser)
SQL 문자열을 받아 문법적으로 올바른지 검사하고, 파스 트리(Parse Tree)로 변환합니다. 이 단계에서 문법 오류가 발생합니다.
ERROR 1064 (42000): You have an error in your SQL syntax ...
4-2. 전처리기 (Preprocessor)
파스 트리를 받아 의미론적 검사를 수행합니다. 테이블이 실제로 존재하는지, 컬럼명이 올바른지, 접근 권한이 있는지 등을 확인합니다. 파서가 문법을 보는 단계라면, 전처리기는 의미를 보는 단계입니다.
ERROR 1146 (42S02): Table 'db.table' doesn't exist
4-3. 옵티마이저 (Optimizer)
MySQL에서 가장 핵심적인 컴포넌트입니다. 동일한 결과를 낼 수 있는 여러 실행 방법 중 가장 비용이 낮은 실행 계획을 선택합니다. 대표적으로 다음을 결정합니다.
- 어떤 인덱스를 사용할지
- 조인이 있을 때 어느 테이블을 먼저 읽을지
- 인덱스를 쓰는 게 유리한지, 풀 스캔이 나은지
옵티마이저는 통계 정보(인덱스 카디널리티, 테이블 행 수 등)를 바탕으로 비용을 추정합니다. 이 추정이 항상 정확하지는 않기 때문에, EXPLAIN으로 실행 계획을 직접 확인하는 습관이 중요합니다.
4-4. 실행 엔진 (Execution Engine)
옵티마이저가 무엇을 할지 결정했다면, 실행 엔진은 그것을 실제로 수행합니다. 실행 엔진은 핸들러 API를 통해 스토리지 엔진에 데이터를 요청하고, 받아온 결과를 조합해 클라이언트에게 반환합니다.
5. 메모리 구조
MySQL의 메모리는 글로벌 메모리와 세션 메모리로 구분됩니다. 이 구분을 이해하면 커넥션 수가 늘어날 때 메모리가 어떻게 소비되는지 파악하는 데 도움이 됩니다.
5-1. 글로벌 메모리 (Global Memory)
MySQL 서버 전체가 공유하는 메모리 영역입니다. 서버가 시작될 때 할당되며, 커넥션 수와 무관하게 고정적으로 사용됩니다.
| 영역 | 설명 |
| 버퍼 풀 (Buffer Pool) | InnoDB의 핵심. 디스크에서 읽은 데이터 페이지와 인덱스를 캐싱 |
| 리두 로그 버퍼 (Redo Log Buffer) | 디스크에 기록되기 전 리두 로그를 임시로 보관 |
| 딕셔너리 캐시 (Dictionary Cache) | 테이블 스키마, 인덱스 메타데이터 등을 캐싱 |
버퍼 풀은 InnoDB 성능에 가장 직접적인 영향을 주는 영역으로, 2부에서 자세히 다룹니다.
5-2. 세션 메모리 (Session Memory)
클라이언트 커넥션(포그라운드 스레드)마다 독립적으로 할당되는 메모리 영역입니다. 커넥션이 늘어날수록 함께 증가한다는 점이 글로벌 메모리와 다릅니다.
| 영역 | 설명 |
| 정렬 버퍼 (Sort Buffer) | ORDER BY, GROUP BY 처리 시 사용 |
| 조인 버퍼 (Join Buffer) | 인덱스를 사용하지 못하는 조인 처리 시 사용 |
| 읽기 버퍼 (Read Buffer) | 풀 테이블 스캔 시 순차 읽기 성능 향상 |
| 바이너리 로그 캐시 | 트랜잭션 중 발생한 바이너리 로그를 임시 보관 |
세션 메모리는 쿼리가 실행되는 동안 필요에 따라 할당되고, 완료되면 해제됩니다. 커넥션당 수십 MB씩 잡힌다면, 커넥션이 수백 개일 때 전체 메모리 사용량이 예상보다 훨씬 커질 수 있습니다.
6. 쿼리 캐시 — 그리고 왜 사라졌는가
MySQL 8.0 이전까지는 쿼리 캐시(Query Cache)가 존재했습니다. 동일한 SELECT 쿼리가 들어오면 파싱이나 실행 없이 캐싱된 결과를 바로 반환하는 기능으로, 커넥션 직후 파서보다도 먼저 확인합니다.
아이디어는 좋았지만, 실제로는 두 가지 구조적 문제가 있었습니다.
캐시 무효화 비용: 테이블에 단 한 건의 쓰기(INSERT/UPDATE/DELETE)가 발생하면, 그 테이블과 관련된 모든 캐시 항목을 즉시 무효화해야 합니다. 쓰기가 잦은 서비스에서는 캐시가 쌓이는 속도보다 무효화되는 속도가 더 빠릅니다.
글로벌 뮤텍스 경합: 캐시를 읽거나 무효화할 때 글로벌 잠금이 필요합니다. 동시 요청이 많아질수록 잠금 경합이 심해져, 오히려 성능 병목이 되었습니다.
결국 MySQL 5.7에서 deprecated, MySQL 8.0에서 완전히 제거되었습니다. 현재는 애플리케이션 레벨에서 Redis 등 별도 캐시 레이어를 두는 것이 일반적입니다.
정리
MySQL 서버 계층은 포그라운드/백그라운드 스레드가 역할을 나누고, 파서 → 전처리기 → 옵티마이저 → 실행 엔진 순으로 쿼리를 처리합니다. 실행 엔진은 핸들러 API를 통해 스토리지 엔진과 통신하며, 이 추상화 덕분에 스토리지 엔진은 플러그인처럼 교체할 수 있습니다. 메모리는 서버 전체가 공유하는 글로벌 영역과 커넥션마다 독립적으로 할당되는 세션 영역으로 나뉘며, 커넥션 수가 늘어날수록 세션 메모리가 누적된다는 점을 주의해야 합니다.
다음 글에서는 스토리지 엔진의 사실상 표준인 InnoDB 내부 구조 — 버퍼 풀, 리두/언두 로그, MVCC, 트랜잭션 격리 수준까지 살펴봅니다.
다음 글: [MySQL 아키텍처 2편 — InnoDB 스토리지 엔진]
'Data > MySQL' 카테고리의 다른 글
| [MySQL] B-Tree 인덱스 완전 해부 — 구조부터 가용성까지 (1) | 2026.06.07 |
|---|---|
| [MySQL] 트랜잭션과 잠금 2편 — 격리 수준과 MVCC (0) | 2026.06.07 |
| [MySQL] 트랜잭션과 잠금 1편 — 락 메커니즘 (0) | 2026.06.07 |
| [MySQL] 아키텍처 2편 — InnoDB 스토리지 엔진 (0) | 2026.06.07 |