MySQL의 전체 구조

MySQL은 일반 상용 RDBMS와 같이 대부분의 프로그래밍 언어로부터 접근 방법을 모두 지원합니다. 이는 MySQL 고유의 C API로부터 시작해서 JDBC나 ODBC, 그리고 .NET의 표준 드라이버를 제공해서 이를 바탕으로 거의 모든 언어로 MySQL 서버에서 쿼리를 사용할 수 있습니다.
MySQL 엔진
MySQL 엔진은 클라이언트로부터의 접속 및 쿼리 요청을 처리하는 커넥션 핸들러와 SQL 파서 및 전처리기, 쿼리의 최적화된 실행을 위한 옵티마이저가 중심을 이룹니다.
- 커넥션 핸들러
- 클라이언트의 접속 요청 (TCP) 수락
- 사용자 인증, 권한 확인
- 연결마다 전담 스레드를 할당, 연결 종료 처리
- 클라이언트가 보내는 쿼리를 MySQL 내부로 전달, 해당 쿼리의 결과나 에러를 클라이언트에게 반환
- 옵티마이저
- 파서를 거쳐서 파서 트리를 생성하고, 전처리기로 컬럼/테이블 이름 해결 등을 수행한 뒤에 최소한의 비용의 계획을 수립하고 실행 엔진으로 넘겨줍니다.
스토리지 엔진
실제 데이터를 디스크 스토리지에 저장하거나 데이터를 읽어오는 부분은 스토리지 엔진이 전담합니다. MySQL 엔진은 하나지만 스토리지 엔진은 여러 개를 동시에 사용할 수 있습니다. 각 스토리지 엔진은 성능 향상을 위해 키 캐시나 InnoDB 버퍼 풀과 같은 기능을 내장하고 있습니다.
핸들러 API
MySQL 엔진에서 스토리지 엔진에 읽기 또는 쓰기를 요청할 때, 이 요청을 핸들러 요청이라고 합니다. 여기서 사용되는 API를 핸들러 API라고 합니다.
MySQL 스레딩 구조

MySQL 서버는 스레드 기반으로 작동합니다. 이를 포그라운드 스레드와 백그라운드 스레드로 구분할 수 있습니다.

확인해보면 BACKGROUND와 FOREGROUND로 구성돼있고, 대부분이 BACKGROUND 스레드임을 확인할 수 있습니다. 동일한 이름의 스레드가 여러 개인 경우도 확인되는데요, (innodb/io_read_thread 등) 이는 병렬 처리를 위한 경우로 볼 수 있습니다.
Forground thread (Client thread)
43 thread/sql/event_scheduler FOREGROUND event_scheduler localhost
47 thread/sql/compress_gtid_table FOREGROUND
1752 thread/sql/one_connection FOREGROUND root [IPv4]
1753 thread/sql/one_connection FOREGROUND root [IPv4]
1755 thread/sql/one_connection FOREGROUND root [IPv4]
이 5개만이 포그라운드 스레드인데, 실제 사용자의 요청을 처리하는 스레드는 one_connection입니다. 주로 각 클라이언트가 요청하는 쿼리 문장을 처리합니다. 클라이언트가 작업을 마치고 커넥션을 종료하면 해당 커넥션을 담당하던 스레드는 다시 스레드 캐시로 되돌아갑니다. 이때 이미 스레드 캐시에 일정 개수 이상의 대기 중인 스레드가 있다면 스레드 캐시에 넣지 않고 스레드를 종료시켜 일정 개수의 스레드만 스레드 캐시에 존재하게 합니다. 마치 스레드 풀과 같은 역할을 한다고 볼 수 있습니다. 이때 스레드 캐시의 최대 스레드 유지 가능 개수는 thread_cache_size 시스템 변수로 설정할 수 있습니다.
스토리지 엔진에 따라서 데이터를 버퍼나 캐시에 데이터가 없으면 직접 디스크에서 가져올 수도 있고, 버퍼나 캐시에 데이터가 확인되지 않으면 디스크 I/O는 백그라운드 스레드가 처리합니다.
Background thread
MyISAM의 경우에는 백그라운드 작업이 거의 없고, InnoDB에서는 많은 작업이 수행됩니다. 다음은 그 예시입니다.
- Insert Buffer를 병합하는 스레드
- 로그를 디스크로 기록하는 스레드
- InnoDB 버퍼 풀의 데이터를 디스크에 기록하는 스레드
- 데이터를 버퍼로 읽어 오는 스레드
- 잠금이나 데드락을 모니터링하는 스레드
데이터 쓰기와 읽기를 수행하는 스레드 (위에서 확인했던 innodb_write_io_thread, innodb_read_io_thread)는 2개 이상을 지정할 수 있습니다. 보통 읽기 작업은 포그라운드 스레드에서 처리돼 많이 설정할 필요가 없지만 쓰기 스레드는 많은 작업을 백그라운드로 처리하기 때문에 일반적인 내장 디스크의 경우 2~4개, DAS와 SAN과 같은 스토리지를 사용할 때는 충분하게 많이 설정하는 것이 좋습니다.
메모리 할당 및 사용 구조

MySQL에서 사용하는 메모리 공간은 글로벌 메모리 영역과 로컬 메모리 영역으로 구분할 수 있습니다. MySQL 서버가 사용하는 메모리의 양을 측정하는 것은 쉽지 않습니다. OS의 정책에 따라서 서버 로드 시에 전부 할당해 줄 수도, 부분 씩 나누어가며 할당해줄 수도 있는 등의 OS에 따른 복잡한 메모리 할당 방식 때문입니다. 따라서 시스템 변수로 설정해 둔 만큼 할당 받는다고 생각해도 무방합니다.
글로벌 메모리 영역
- 테이블 캐시
- InnoDB 버퍼 풀
- InnoDB 어댑티브 해시 인덱스
- InnoDB 리두 로그 버퍼
등이 있습니다. 일반적으로 하나의 메모리 공간만 할당되고, 2개 이상의 공간을 할당 받을 수도 있지만 어차피 전부 공유됩니다.
로컬 메모리 영역
클라이언트 스레드, 즉 포그라운드 스레드가 쿼리를 처리하는 데 사용하는 메모리 영역입니다. 클라이언트가 MySQL 서버에 접속하면 클라이언트 커넥션으로부터 요청을 처리하기 위해서 스레드를 하나씩 할당하게 됩니다. 클라이언트와 MySQL 서버와의 커넥션을 세션이라고 하기 때문에 세션 메모리 영역이라고도 표현합니다.
로컬 메모리는 스레드별로 독립적으로 할당되며 절대 공유되지 않습니다. 또한 각 쿼리의 용도별로 필요할 때만 공간이 할당되며 필요하지 않은 경우에는 MySQL이 메모리 공간을 아얘 할당하지 않을 수도 있습니다. 조인 버퍼, 소트 버퍼 등이 이에 해당하고, 실행하는 순간에만 할당했다가 해제하는 방법을 사용합니다. 커넥션이 열려있는 동안은 커넥션 버퍼나 결과 버퍼는 계속 할당된 상태로 남아있습니다.
플러그인 스토리지 엔진 모델
플러그 인, 아웃으로 다양한 기능을 플러그인 형태로 쉽게 개발 및 사용할 수 있습니다. 스토리지 엔진 뿐만 아니라 사용자 인증을 위한 Caching SHA-2 Authentication 등도 모두 플러그인을 통해서 구현되어 제공됩니다.
여기서 MySQL 엔진이 각 스토리지 엔진에게 데이터를 읽어오거나 저장하도록 명령하려면 반드시 핸들러를 통해야하고, 이런 명령의 횟수는 MySQL 서버의 상태 변수로 확인할 수 있습니다. 스토리지 엔진에서 중요한 점은, 하나의 쿼리 작업은 여러 하위 작업으로 나뉘는데, 각 하위 작업이 MySQL 엔진 영역에서 처리되는지 아니면 스토리지 엔진 영역에서 처리되는지 구분할 줄 알아야 한다는 점입니다.

플러그인 아키텍처는 높은 확장성과 책임 분리 등의 장점을 가지고 있지만, 단점도 존재합니다.
- 플러그인은 오직 MySQL 서버와 인터페이스할 수 있고, 플러그인끼리는 통신 불가
- 플러그인은 MySQL 서버의 변수나 함수를 직접 호출하기 때문에 캡슐화가 안돼 안전하지 않음
- 플러그인은 상호 의존 관계를 설정할 수 없음
이러한 문제를 해결하고자 MySQL 8.0부터 컴포넌트 아키텍처를 지원하게 됐습니다.
- 컴포넌트는 서비스 인터페이스를 통해서 다른 컴포넌트와 통신이 가능
- 서버 측에서 제공하는 서비스 API만 사용이 가능해서, 서버 내부 변수 및 함수에 직접 접근이 금지
- 컴포넌트는 manifest 파일에 명시적으로 의존성을 적어서 관계 선언 가능
Error log와 같은 기능은 이미 컴포넌트 기반으로 완전히 재설계됐으며, 플러그인 API는 더 이상 사용되지 않는 방향으로 가고 있습니다.
쿼리 실행 구조

쿼리 파서
요청으로 들어온 쿼리 문장을 토큰으로 분리해서 트리 형태로 만듭니다. 기본 문법 오류는 여기서 발견되고 처리됩니다.

전처리기
파서 트리를 바탕으로 쿼리 문장에 구조적인 문제점이 있는지 확인합니다. 토큰을 테이블 이름이나 컬럼 이름과 같은 객체와 매핑하는 작업을 수행하고, 여기서 객체의 존재 여부나 객체의 접근 권한 등을 확인합니다.
옵티마이저
쿼리를 저렴한 비용으로 가장 빠르게 처리할수 있도록 결정하는 역할을 담당합니다. 옵티마이저가 더 나은 선택을 할 수 있게 유도하는 것이 중요합니다.
실행 엔진
실행 엔진은 만들어진 계획대로 핸들러에게 요청해서 받은 결과를 또 다른 핸들러 요청의 입력으로 연결하는 역할을 수행합니다. MySQL 엔진과 핸들러 API 사이의 매개체라고 생각하면 좋습니다.
쿼리 캐시
SQL의 실행 결과를 메모리에 캐시하고 동일 SQL 쿼리가 실행되면 테이블을 읽지 않고 즉시 결과를 반환하도록 설계된 캐시 기능입니다. 직관적인 캐시의 기능으로 특정 상황에서는 매우 빠른 성능을 보였으나, 데이터 변경이 일어나면 많은 부분이 invalidate 돼서 심각한 동시 처리 성능 저하를 유발합니다. 이로 인한 버그도 많이 발생됐기 때문에 8.0 버전부터는 쿼리 캐시가 완전히 제거됐습니다.
스레드 풀
MySQL 엔터프라이즈 에디션부터 사용할 수 있는 기능으로, Percona Server의 스레드 풀 플러그인을 MySQL 커뮤니티 에디션에 플러그인으로 설치해서 사용할 수도 있습니다. 스레드 풀은 기본적으로 서버의 자원 소모를 최적화하는 것이 목적입니다. 즉, 설치하는 것만으로 성능이 무작정 올라가지 않습니다. 동시에 실행 중인 스레드들을 CPU가 최대한 효율적으로 처리할 수 있는 수준으로 줄여서 빨리 처리하는게 목적인 만큼, CPU 시간을 제대로 할당받지 못하면 스레드 수의 최대치가 애초에 적기 때문에 쿼리 처리가 더 느려지는 현상이 생길 수 있습니다. 따라서 스레드 풀을 사용할 때는 사용 중인 CPU에 친화적으로 설계해서 컨텍스트 스위칭을 최소화할 수 있도록 한 뒤에 사용해야합니다. 그래서 Percona Server의 스레드 풀은 기본적으로 CPU 코어의 개수만큼 스레드 그룹을 생성합니다.
스레드 풀에 작동에 대해서 조금 더 다뤄보겠습니다. 만약 스레드 그룹의 모든 스레드가 일을 처리하고 있다면 Idle thread가 생길 때까지 기다릴지, worker thread를 추가할지 판단해야 합니다. 이때 스레드 풀 타이머가 판단의 역할을 수행해서, thread_pool_stall_limit만큼의 주기로 스레드 그룹의 상태를 체크해 스레드가 작업 중일 경우 worker thread를 스레드 그룹에 추가합니다. 즉, 주기적으로 스레드 그룹을 감시해서 전부 Busy인 상태가 생기면 그 때 worker thread를 추가합니다. 이때 생성된 스레드 개수가 thread_pool_max_threads를 넘을 수 없으므로 이 만큼의 스레드가 이미 생성된 경우에는 자리가 빌 때까지 기다리게 됩니다. 스레드가 작업을 끝내면 콜백으로 즉시 다음 태스크를 가져오므로 이 타이머는 Long-running 쿼리가 CPU를 독점해서 콜백이 안 오는 상황을 보완하기 위한 보조 메커니즘으로 사용됩니다. 이와 관련해서, 응답 지연 시간에 예민한 서비스의 경우에 thread_pool_stall_limit를 기본값보다 줄여서 스레드 풀이 꽉 찼을 때 worker thread를 빠르게 만들어서 큐잉 딜레이를 줄이는 튜닝을 고려할 수 있습니다.
트랜잭션 지원 메타데이터
데이터베이스 서버에서테이블의 구조 정보와 스토어드 프로그램 등의 정보를 말합니다. 8.0 이전 버전에서는 트랜잭션이 지원되지 않는 파일 기반 관리였지만, 8.0 부터는 InnoDB의 테이블에 메타데이터를 저장하도록 개선돼서 비정상적인 종료로 데이터베이스가 깨지는 현상을 없앴습니다. 메타데이터에는 테이블의 구조 정보, 스토어드 프로그램의 코드 관련 정보, 서버 작동에 필요한 테이블 정보인 시스템 테이블 정보가 있고, 이는 모두 mysql.ibd에 저장됩니다.
데이터 딕셔너리, 즉 메타데이터가 InnoDB에게 위임돼면서 트랜잭션 기반으로 처리되도록 변경됐기 때문에 변경에 대한 결과가 원자적으로 형성됩니다. 즉, 완전한 실패 또는 완전한 성공으로 처리돼서 어중간하게 깨지는 현상이 없는 것입니다. InnoDB가 아닌 MyISAM이나 CSV 등과 같은 스토리지 엔진의 메타 정보는 여전히 파일 형태로 저장되고, 이는 SDI(Serialized Dictionary Information) 확장자를 사용합니다. 확장자의 이름에서 알 수 있듯이 데이터 딕셔너리를 직렬화한 정보이므로 ibd2sdi 유틸리티를 이용하면 각종 스토리지 엔진의 스키마 정보를 직렬화해서 저장할 수 있습니다.
❓스레드 풀 vs 스레드 캐시
스레드 캐시
새 연결이 올 때마다 스레드를 생성하는 대신에 이전에 연결이 끊긴 스레드를 제거하지 않고 재활용하는 방식입니다. 이에 따라서 스레드 생성 비용과 제거 비용을 아낄 수 있습니다. 다만 max_threads와 같은 최대값 개념이 없기 때문에 동시에 실행되는 쿼리의 개수는 제한없이 늘어날 수 있습니다.
스레드 풀
스레드를 재사용하기도 하면서, 동시에 실행되는 실제 작업 스레드를 제한해서 CPU 컨텍스트 스위칭을 최소화하고 긴 쿼리가 서버를 독점하지 못하도록(Timer 기반) 합니다. 또한 작업 스레드가 코어 수에 따라 최소화됨에 따라서 CPU 캐시 히트율도 올라가 성능이 더 좋아질 수 있습니다.
대부분의 웹 서비스의 경우 스레드 풀이 tail latency와 throughput 안정성 측면에서 스레드 캐시보다 높은 성능을 보입니다.
❓DAS, NAS, SAN
DAS (Direct Attached Storage)
서버에 직접 꽂은 외장 하드
USB나 SATA로 직접 연결해서 사용하는 방식으로 빠르고 단순하지만, 한 대의 서버만 쓸 수 있습니다. OS가 파일시스템을 직접 관리합니다.
NAS (Network Attached Storage)
집에 공유 폴더를 만들어 놓은 파일 서버
네트워크(LAN)로 여러 컴퓨터가 동시에 파일 읽고 쓰기 가능하도록 만든 스토리지입니다. NAS 장치가 파일 시스템을 관리하고, 클라이언트는 파일 및 디렉토리 단위로 요청할 수 있습니다. 공유 폴더, 사진 백업, 미디어 서버 등에 사용됩니다.
SAN (Storage Area Network)
기업용 초고속 통로를 통한 여러 서버의 디스크 블록 공유
마치 서버들이 하나의 거대한 로컬 SSD를 쓰는 것처럼 동작헤서 개개의 서버 입장에서는 로컬 디스크처럼 보입니다. 파일시스템은 각 서버가 독립적으로 관리합니다. 데이터베이스, VM, 고성능 앱에 사용됩니다.
참고 문헌
Real MySQL 8.0
'Backend > DB' 카테고리의 다른 글
| [DB] 데이터 모델링 (0) | 2026.05.30 |
|---|---|
| [DB] 비관적 락과 낙관적 락 (0) | 2026.05.01 |
| [MySQL] MySQL에서의 트랜잭션과 잠금 분석 (0) | 2026.03.26 |
| [MySQL] InnoDB 스토리지 엔진 아키텍처 분석 (0) | 2026.03.16 |