개발을 하다 보면 Spring
과 MySQL
을 함께 사용하는 경우가 많다.
나 역시 프로젝트를 진행하면서 DB와 연결해 데이터를 주고받는 작업은 익숙하지만 정작 내부적으로 어떻게 처리되는지에 대해서는 깊이 알지 못한 채 사용해왔다.
게다가 MySQL 8.0 이후 많은 구조적 변화가 있었고 스토리지 엔진별 차이나 쿼리 실행 흐름도 성능에 큰 영향을 줄 수 있다는 이야기를 자주 접하게 됐다.
하지만 정작 그 차이를 명확히 설명하거나 상황에 맞는 DB를 선택할 기준을 세우기는 쉽지 않았다.
이번 글에서는 단순한 사용을 넘어서 MySQL의 아키텍처 구조와 쿼리 실행 과정, 스토리지 엔진의 차이를 하나씩 정리해보려 한다.
이를 통해 DB를 더 잘 이해하고 설계와 선택에 자신감을 가질 수 있는 기반을 마련하고자 한다.
MySQL은 크게 4가지 계층으로 구성되어 있다.
사용자가 MySQL 서버에 접근하기 위한 다양한 방식들을 담당한다.
MySQL은 다양한 언어와 플랫폼을 지원하기 때문에, 프로그래밍으로 작성된 코드도 이 계층을 통해 MySQL에 접속하고 쿼리를 실행할 수 있다.
사용자의 SQL 요청을 받아 서버에 전달하는 역할로 클라이언트와 서버 간 통신은 TCP/IP를 기반으로 이루어진다.
MySQL 엔진은 클라이언트로부터 전달받은 SQL 쿼리를 분석한다.
또한 어떻게 실행할지 결정하고 실행하는 중심 계층이면서 MySQL의 핵심 로직 대부분이 이 레이어에 존재한다.
우리가 작성하는 대부분의 SQL 쿼리는 이 계층에서 해석되고 실행된다.
꼭 SQL 문장을 쓰지 않아도 NoSQL처럼 간단한 명령으로 데이터를 다루는 유연한 방식도 가능하다는 의미
- 데이터를 조회(DML), 테이블 생성(DDL), 권한 설정(DCL) 등 다양한 쿼리를 보냄.
- 뷰(View), 트리거(Trigger), 저장 프로시저(Procedure) 같은 고급 SQL 기능 처리.
- 쿼리를 받아서 다음 단계인 Parser(파서)로 넘겨주는 역할.
SQL 문장을 정리하고 검사하는 전처리 단계
MySQL이 쿼리를 이해하고 실행 계획을 세우는 두뇌 역할
스토리지 엔진은 실제 데이터를 저장하고, 불러오고, 수정하는 역할을 맡는 계층이다.
사용자가 보낸 쿼리는 SQL 엔진을 거쳐 이곳까지 도달하고 스토리지 엔진이 최종적으로 데이터를 디스크에 저장하거나 조회함.
추가로 MySQL은 하나의 DBMS 안에서 다양한 스토리지 엔진을 사용할 수 있는 구조로 설계되어있다. (MySQL의 특징)
엔진 | 설명 |
---|---|
InnoDB | MySQL의 기본 스토리지 엔진. 트랜잭션, 외래 키, 롤백 등 고급 기능 지원. 실무에서 가장 많이 사용. |
MyISAM | 예전 기본 엔진. 빠른 읽기 성능이 강점이지만, 트랜잭션 미지원. |
Memory | 데이터를 메모리에 저장해 속도가 빠르지만, 서버 재시작 시 데이터 유실됨. |
CSV | 테이블을 CSV 파일처럼 저장. 외부 시스템과 연동할 때 사용됨. |
Archive | 많은 양의 데이터를 압축해 저장. 주로 로그 데이터 보관 용도. |
MySQL의 마지막 단계는 결국 운영체제와 물리적인 저장소(Disk)다.
스토리지 엔진이 데이터를 처리하면, 그 결과는 결국 파일 시스템을 통해 디스크에 저장한다.
옵티마이저가 만든 실행 계획대로 스토리지 엔진을 호출해서 레코드를 읽고 쓴다.
실행 엔진은 "무엇을 할지"가 아니라, "어떻게 할지"를 직접 실행하는 행동 주체다.
스토리지 엔진은 실제로 데이터를 저장하고 읽어오는 최종 계층이다.
쿼리 실행 엔진이 요청하면 스토리지 엔진은 그 명령에 따라 디스크에 접근해 데이터를 처리한다.
핸들러(Handler)란?
스토리지 엔진은 Handler API라는 내부 인터페이스를 통해 SQL 엔진과 연결된다. MySQL 내부에서는 스토리지 엔진 자체를 핸들러(handler)라고 부르기도 한다.
MySQL 8.0부터는 InnoDB가 기본 스토리지 엔진으로 채택되었으며 트랜잭션, 외래 키, MVCC, 충돌 제어 같은 고급 기능을 지원하는 강력한 저장 엔진이다.
단순히 데이터를 디스크에 저장하는 데 그치지 않고 메모리 관리, 로그 처리, 쓰기 최적화까지 수행하는 복잡한 내부 구조를 갖고 있다.
핵심 특징을 정리하면
InnoDB는 테이블의 데이터를 기본 키(Primary Key, PK) 순서대로 정렬하여 디스크에 저장한다.
이걸 클러스터링 인덱스 구조라고 부르며 InnoDB의 가장 큰 특징 중 하나다.
주의할 점
레코드를 정렬된 상태로 유지해야 하므로, 삽입/갱신 시 디스크 I/O 비용이 발생
→ 특히 중간 위치에 데이터를 삽입하거나 PK를 변경할 경우 쓰기 성능이 저하될 수 있음
PK를 자주 수정하거나, 랜덤한 값(UUID 등)을 PK로 사용하는 경우
→ 클러스터링의 장점을 제대로 활용하지 못하고 쓰기 병목이 발생할 수 있음
MVCC는 동시에 여러 트랜잭션이 데이터를 접근할 수 있도록 허용하면서도, 데이터의 일관성을 유지하기 위한 기술이다.
InnoDB는 이 기능을 통해 트랜잭션 간 락(Lock) 없이 읽기 작업을 가능하게 만들어준다.
그림을 통해 이해해보자.
상황 요약
- USER 테이블: id, name, jab 컬럼 존재
- 기존 데이터 (커밋됨):
id: 1, name: Jay, jab: 기획자
- 이후 트랜잭션 T2에서 다음과 같이 jab을 '개발자'로 수정함:
UPDATE USER SET jab = '개발자' WHERE id = 1;
- 이 변경이 커밋되기 전/후에 따라 다른 트랜잭션(T1)이 어떤 값을 읽는지는 트랜잭션 격리 수준(Isolation Level)에 따라 달라짐
트랜잭션 격리 수준 | 커밋 전 읽기 결과 | 커밋 후 읽기 결과 | 설명 |
---|---|---|---|
READ UNCOMMITTED | '개발자' (변경값) | '개발자' | 커밋되지 않은 변경 내용도 읽을 수 있음 → Dirty Read 발생 가능 |
READ COMMITTED | '기획자' (이전값) | '개발자' | 커밋된 내용만 읽음. 커밋 전엔 이전 값, 커밋 후엔 최신 값 읽음 |
REPEATABLE READ (InnoDB 기본값) | '기획자' (이전값) | '기획자' (계속 동일) | 트랜잭션 시작 시점의 스냅샷을 유지 → 반복 조회 시 항상 같은 값 |
SERIALIZABLE | '기획자' (이전값) | '기획자' (혹은 락 대기) | REPEATABLE READ처럼 보이지만, 실제론 읽기에도 락을 걸어서 완벽한 직렬화 보장 |
InnoDB는 ACID를 보장하기 위해 Undo Log와 Redo Log라는 두 가지 핵심 로그 시스템을 사용한다.
InnoDB는 데이터를 읽거나 수정할 때, 테이블 전체가 아닌 "해당 레코드(행)"만 잠그는 방식을 사용한다.
이를 레코드 단위 잠금(또는 행 잠금, Row Lock)이라고 부른다.
예시 DB 설명
- 총 1000명 학생
- 학년은 1학년부터 4학년
- 2학년 학생 수: 600명
- grade(학년) 컬럼에 인덱스 생성
- id는 PK(Primary Key)
- 제주도에 사는 학생은 1명 밖에 없음.
address 가 제주도인 학생의 주소를 변경하는 쿼리를 작성할 때 grade(학년) 인덱스를 사용하게 되는데 이때 어떤 레코드가 잠기게 될까?
학년이 2학년이 모든 레코드를 잠그는 방식으로 처리된다.
id 인덱스를 사용할 때는 어떤 레코드가 잠기게 될까?
모든 데이터인 1000개의 레코드가 잠기게 된다. (풀스캔)
grade 와 address에 대한 복합 인덱스를 생성했다면?
제주도에 사는 학생은 1명 밖에 없기 때문에 그림과 같이 1개의 레코드만 잠기게된다.
이렇게 인덱스를 어떻게 설정 하는지에 따라 레코드의 잠금 범위가 달라질 수 있다.
InnoDB는 디스크 기반 스토리지 엔진이지만, 성능을 최대한 높이기 위해 메모리 캐시 계층을 적극적으로 활용한다.
대표적인 구조가 버퍼 풀(Buffer Pool)과 어댑티브 해시 인덱스(Adaptive Hash Index, AHI)다.
버퍼 풀은 InnoDB의 핵심 메모리 캐시 영역으로 디스크의 데이터를 페이지 단위(기본 16KB)로 불러와 메모리에 저장하고 관리한다.
InnoDB는 자주 접근되는 인덱스 페이지를 더 빠르게 접근하기 위해 해시 테이블 기반의 인덱스를 자동으로 만들어 사용한다.
항목 | 설명 |
---|---|
자료구조 | <인덱스 키, 페이지 주소> 쌍으로 구성된 해시 테이블 |
생성 방식 | 자주 사용되는 인덱스 키 범위에 대해 InnoDB가 자동 생성 |
역할 | B+Tree 탐색 없이, 해시 탐색만으로 해당 페이지에 빠르게 접근 |
동작 시점 | 조회가 반복되는 특정 인덱스 키에 대해 동적으로 생성됨 |
주의점
너무 많은 해시 인덱스가 생성되면 오히려 해시 테이블이 커져서 관리 비용 증가한다. 서버에 따라 자동 생성이 부담되면 설정으로 끌 수도 있음
MyISAM은 MySQL 5.5 이전까지의 기본 스토리지 엔진이었으며 빠른 읽기 성능과 단순한 구조로 인해 한때 많이 사용됐다.
하지만 트랜잭션 처리나 데이터 무결성 지원이 부족하기 때문에 요즘은 정적 데이터, 로그, 읽기 위주 시스템 등에서 제한적으로만 사용된다.
항목 | InnoDB | MyISAM |
---|---|---|
트랜잭션 | ✅ 지원 (COMMIT, ROLLBACK 등) | ❌ 미지원 |
잠금 방식 | ✅ 레코드 단위(Row Lock) 동시성 우수 | ❌ 테이블 단위(Table Lock) 쓰기 충돌 많음 |
외래 키(Foreign Key) | ✅ 지원 | ❌ 미지원 |
MVCC (다중 버전) | ✅ 지원 (REPEATABLE READ 기본) | ❌ 미지원 |
데이터 무결성 | ✅ 강력 (제약조건 + 복구 메커니즘) | ❌ 약함 (제약 조건 없음) |
장애 복구 | ✅ Redo/Undo 로그를 통한 복구 가능 | ❌ 복구 기능 거의 없음, 데이터 손상 시 취약 |
읽기 성능 | 🔼 빠르지만 구조가 복잡 (쓰기와 동시성 고려) | 🔼 매우 빠름 (구조 단순, 경량화) |
쓰기 성능 | ⚠️ 레코드 단위 락으로 상대적으로 느릴 수 있음 | ❌ 테이블 락 때문에 쓰기 동시성 매우 낮음 |
전체 텍스트 검색 (FULLTEXT) | ✅ MySQL 5.6부터 지원 | ✅ 원래부터 지원 (빠름) |
스토리지 구조 | .ibd 파일 (데이터+인덱스 통합) | .MYD (데이터), .MYI (인덱스), .frm (메타) |
권장 용도 | 대부분의 OLTP 시스템, 일반적인 웹/앱 서비스 | 읽기 위주 정적 테이블, 임시 테이블, 로그 등 |
현재 위치 | ✅ MySQL 기본 스토리지 엔진 (8.0 기준) | ⚠️ 레거시 / 특수한 경우만 사용 |