한 줄 정의
MySQL 엔진은 SQL을 파싱하고 최적화하는 두뇌 역할이며, 스레드 기반으로 동작하고, 메모리를 글로벌/로컬 영역으로 나누어 관리합니다.
쉽게 말하면
MySQL 서버에 쿼리가 들어오면, 그 쿼리가 실제 디스크 데이터에 닿기까지 여러 단계를 거칩니다. 마치 공장의 생산 라인처럼 각 단계마다 전담 부서가 있고, 이 파이프라인 전체를 관리하는 것이 MySQL 엔진입니다.
스토리지 엔진은 이 파이프라인의 마지막 단계인 “데이터 읽기/쓰기”만 담당합니다. GROUP BY, ORDER BY 같은 복잡한 처리는 전부 MySQL 엔진의 쿼리 실행기에서 처리됩니다.
왜 중요한가?
쿼리 튜닝을 할 때 병목이 어디서 발생하는지 판단하려면 이 구조를 알아야 합니다.
- 스토리지 엔진에서 느린 것인지(인덱스 미사용, 디스크 I/O) → 인덱스 튜닝
- MySQL 엔진에서 느린 것인지(filesort, 임시 테이블) → 쿼리 구조 변경
- 스레드/메모리 설정 문제인지 → 시스템 변수 튜닝
핵심 내용
전체 구조
flowchart TB subgraph Client["클라이언트"] JDBC["JDBC / ODBC / C API"] end subgraph MySQL_Engine["MySQL 엔진 (두뇌)"] ConnHandler["커넥션 핸들러"] Parser["쿼리 파서"] Preprocessor["전처리기"] Optimizer["옵티마이저"] Executor["실행 엔진 (쿼리 실행기)"] end subgraph Storage["스토리지 엔진 (손발)"] InnoDB["InnoDB"] MyISAM["MyISAM"] Others["MEMORY / CSV / ..."] end Client --> ConnHandler ConnHandler --> Parser Parser --> Preprocessor Preprocessor --> Optimizer Optimizer --> Executor Executor -->|핸들러 API| Storage Storage --> Disk[(디스크)]
MySQL 엔진은 하나 이지만, 스토리지 엔진은 테이블별로 다르게 지정할 수 있습니다.
CREATE TABLE test_table (fd1 INT, fd2 INT) ENGINE=INNODB;MySQL 엔진과 스토리지 엔진 사이의 통신은 핸들러 API 를 통해 이루어집니다. SHOW GLOBAL STATUS LIKE 'Handler%' 명령으로 핸들러 호출 횟수를 확인할 수 있습니다.
스레딩 구조
MySQL은 프로세스 기반이 아닌 스레드 기반 으로 동작합니다.
포그라운드 스레드 (= 사용자 스레드)
- 클라이언트 커넥션마다 하나씩 할당됩니다
- 쿼리 처리를 담당하며, 데이터를 버퍼/캐시에서 가져옵니다
- 커넥션 종료 시 스레드 캐시 로 반환됩니다 (
thread_cache_size로 최대 개수 설정) - InnoDB 테이블: 데이터 버퍼까지만 처리하고, 디스크 쓰기는 백그라운드 스레드에 맡깁니다
- MyISAM 테이블: 디스크 쓰기까지 포그라운드 스레드가 직접 처리합니다
백그라운드 스레드
InnoDB에서 특히 중요한 백그라운드 스레드들입니다:
| 스레드 | 역할 |
|---|---|
| 로그 스레드 | 리두 로그를 디스크에 기록 |
| 쓰기 스레드 | 버퍼 풀의 더티 페이지를 디스크에 기록 (innodb_write_io_threads) |
| 읽기 스레드 | 데이터를 버퍼로 읽어옴 (innodb_read_io_threads) |
| 인서트 버퍼 병합 스레드 | 체인지 버퍼 내용을 실제 인덱스에 병합 |
| 데드락 모니터링 스레드 | 잠금 교착 상태 감지 |
핵심 포인트: 쓰기 작업은 지연(버퍼링)할 수 있지만, 읽기 작업은 절대 지연할 수 없습니다. InnoDB는 이 원칙에 따라 쓰기를 백그라운드로 처리하여 사용자 쿼리의 응답 시간을 줄입니다.
스레드 풀
MySQL 커뮤니티 에디션은 커넥션당 하나의 포그라운드 스레드가 할당되는 전통적 모델을 사용합니다. 스레드 풀 은 엔터프라이즈 에디션이나 Percona Server에서 제공되며, 하나의 스레드가 여러 커넥션 요청을 전담하는 방식입니다.
Percona Server의 스레드 풀은 기본적으로 CPU 코어 수만큼 스레드 그룹을 생성하고(thread_pool_size), 각 그룹에 소속된 커넥션들을 담당합니다. 선순위 큐와 후순위 큐를 이용해 트랜잭션 내 쿼리를 우선 처리합니다.
메모리 할당 구조
MySQL의 메모리 영역은 공유 여부 에 따라 두 가지로 나뉩니다.
flowchart TB subgraph Global["글로벌 메모리 영역 (모든 스레드 공유)"] BP["InnoDB 버퍼 풀"] TC["테이블 캐시"] AHI["어댑티브 해시 인덱스"] RLB["리두 로그 버퍼"] end subgraph Local["로컬 메모리 영역 (스레드별 독립)"] SB["정렬 버퍼 (Sort Buffer)"] JB["조인 버퍼"] CB["커넥션 버퍼"] NB["네트워크 버퍼"] BLC["바이너리 로그 캐시"] end
글로벌 메모리 영역
- MySQL 서버 시작 시 OS로부터 할당됩니다
- 클라이언트 스레드 수와 무관하게 존재합니다
- 모든 스레드가 공유합니다
로컬 메모리 영역 (= 세션 메모리 영역)
- 각 클라이언트 스레드별로 독립 할당되며, 절대 공유되지 않습니다
- 두 가지 할당 패턴이 있습니다:
- 커넥션 유지 동안 상시 할당: 커넥션 버퍼, 결과 버퍼
- 쿼리 실행 시에만 할당: 정렬 버퍼, 조인 버퍼
- 글로벌 메모리는 주의해서 설정하면서 로컬 메모리(예:
sort_buffer_size)는 간과하기 쉽지만, 커넥션 수 x 버퍼 크기 로 총 사용량이 결정되므로 최악의 경우 OOM이 발생할 수 있습니다
쿼리 실행 구조
쿼리가 실행되는 파이프라인을 단계별로 살펴봅니다.
1. 쿼리 파서
SQL 문장을 토큰 (최소 단위의 어휘/기호)으로 분리하여 파서 트리 를 생성합니다. 문법 오류는 이 단계에서 검출됩니다.
2. 전처리기
파서 트리를 기반으로 구조적 문제점을 확인합니다. 테이블/칼럼의 존재 여부, 접근 권한을 이 단계에서 검증합니다.
3. 옵티마이저
쿼리를 가장 적은 비용으로 가장 빠르게 처리할 방법 을 결정합니다. DBMS의 두뇌에 해당하며, 어떤 인덱스를 사용하고, 어떤 순서로 테이블을 조인할지 등을 결정합니다.
4. 실행 엔진 (쿼리 실행기)
옵티마이저가 만든 실행 계획대로 각 핸들러에게 요청을 보내는 관리자 역할입니다.
예를 들어, GROUP BY를 처리하기 위해 임시 테이블이 필요하면:
- 실행 엔진이 핸들러에게 “임시 테이블을 만들어라” 요청
- 핸들러에게 “WHERE 조건에 맞는 레코드를 읽어라” 요청
- 읽어온 레코드를 임시 테이블에 저장하도록 핸들러에게 요청
- 최종 결과를 클라이언트에게 반환
5. 핸들러 (스토리지 엔진)
실행 엔진의 요청에 따라 실제 디스크의 데이터를 읽고 씁니다.
플러그인 스토리지 엔진 모델
MySQL의 독특한 구조 중 하나입니다. 스토리지 엔진뿐 아니라 인증, 전문 검색 파서, 쿼리 재작성 등 다양한 기능이 플러그인 형태로 제공됩니다.
SHOW ENGINES로 지원 스토리지 엔진을, SHOW PLUGINS로 전체 플러그인 목록을 확인할 수 있습니다.
컴포넌트 (MySQL 8.0~)
플러그인의 단점을 보완하기 위해 MySQL 8.0에서 도입된 아키텍처입니다.
| 기준 | 플러그인 | 컴포넌트 |
|---|---|---|
| 상호 통신 | MySQL 서버와만 가능 | 컴포넌트끼리도 가능 |
| 캡슐화 | 서버 변수/함수 직접 호출 (안전하지 않음) | 인터페이스 기반 (캡슐화) |
| 의존성 | 상호 의존 관계 설정 불가 | 의존 관계 관리 가능 |
예: 비밀번호 검증 기능이 5.7에서는 플러그인, 8.0에서는 컴포넌트로 제공됩니다.
INSTALL COMPONENT 'file://component_validate_password';트랜잭션 지원 메타데이터 (MySQL 8.0~)
MySQL 5.7까지는 테이블 구조(메타데이터)를 .FRM 파일로 관리했습니다. 이 파일 기반 메타데이터는 트랜잭션을 지원하지 않아서, DDL 실행 중 서버가 비정상 종료되면 테이블과 실제 데이터 파일이 불일치하는 문제가 있었습니다.
MySQL 8.0부터는 메타데이터를 InnoDB 테이블 에 저장하여 트랜잭션 기반으로 관리합니다. 이로써 스키마 변경 중 비정상 종료에도 일관성이 보장됩니다.
정리
전통적 스레드 모델 vs 스레드 풀
| 기준 | 전통적 모델 | 스레드 풀 |
|---|---|---|
| 스레드:커넥션 관계 | 1:1 | N:1 |
| 컨텍스트 스위칭 | 커넥션 수에 비례하여 증가 | 제한적 스레드로 최소화 |
| 적합한 환경 | 커넥션 수가 적은 환경 | 대량 커넥션, 짧은 쿼리 |
| 제공 | 커뮤니티 에디션 | 엔터프라이즈 / Percona |
글로벌 메모리 vs 로컬 메모리
| 기준 | 글로벌 메모리 | 로컬 메모리 |
|---|---|---|
| 공유 범위 | 모든 스레드 | 해당 스레드만 |
| 할당 시점 | 서버 시작 시 | 커넥션/쿼리 시 |
| 위험 요소 | 설정값 그대로 메모리 점유 | 커넥션 수 × 버퍼 크기 = 총 사용량 |
| 대표 예시 | InnoDB 버퍼 풀 | Sort Buffer, Join Buffer |
내 생각
-
MySQL 엔진과 스토리지 엔진의 역할 구분은
Handler_*상태 변수를 통해 실무에서 바로 확인할 수 있습니다.Handler_read_rnd_next가 높다면 풀 테이블 스캔이 많다는 의미이므로 인덱스 점검이 필요합니다. -
로컬 메모리(특히
sort_buffer_size,join_buffer_size)를 크게 설정하면 커넥션이 몰릴 때 메모리 부족이 발생할 수 있습니다. 기본값을 유지하면서 특정 세션에서만SET SESSION으로 조정하는 것이 안전합니다. -
MySQL 8.0에서 메타데이터가 InnoDB 기반으로 전환된 것은, DDL을 자주 수행하는 마이그레이션 상황에서 안정성을 크게 향상시킵니다. 5.7 이하 버전에서 겪었던 “FRM 파일 불일치” 문제가 사라졌습니다.
관련 개념
출처
- Real MySQL 8.0 (1권), 4.1 MySQL 엔진 아키텍처