MYSQL의 내부 동작과 그 원리에 대해 궁금해져, Real MySQL 8.0 책을 읽었는데 정리를 안하니 계속 내용이 휘발되어서 내 방식으로 정리해보고자 글을 쓴다.
트랜잭션은 데이터의 정합성을 보장하기 위한 기능이다.
1. ACID
데이터 베이스는 트랜잭션의 무결성을 보장하기 위해 ACID(기능)를 만족한다.
- Atomicity (원자성) : 원자성 작업으로, 전체가 Commit되거나 Rollback되어야 한다.
- Consistency (일관성) & Isolation (격리성) : 서로 다른 두 개의 트랜잭션에서 동일 데이터를 조회하고 변경하는 경우에도 상호 간섭이 없어야 한다.
- Durability (지속성) : 한 번 저장된 데이터는 지속적으로 유지가 되어야 한다.
트랜잭션과 관련된 더 다양한 내용이 궁금하다면 아래 글을 참고하자.
https://persi0815.tistory.com/112
[DB] Transactions & Serializability
Transaction 이란? : DB 처리의 논리적 단위를 이루는 실행 프로그램이다. 특징은 다음과 같다. - 하나의 Transaction에는 하나 이상의 DB Access 작업이 포함된다. (삽입, 삭제, 수정, 검색) - 하나의 응
persi0815.tistory.com
2. 트랜잭션은 짧게!
이전 글에서 Undo에 대해 살짝 언급했는데, Undo 로그에 대해 다시 짚어보자면 다음과 같다.
Undo는 데이터 일관성을 위해 어떤 트랜잭션이 어떤 버전의 데이터를 읽어야하는지 알려준다. 하이버네이트에 의해 Flush가 되면, 영속성 컨텍스트(메모리)에 쌓여있던 데이터의 변경 사항을 SQL 쿼리로 변환하여 전송한다. 그러면, 버퍼풀은 새로운 값으로 업데이트가 되는 동시에 이전 데이터는 Undo 로그에 기록된다.이렇게 트랜잭션에 대응되도록 Undo 로그가 생성되고, 실시간으로 디스크에 기록된다.
그리고, repeatable_read, serializable 격리수준을 위해 해당 영역을 필요로 하는 가장 오래된 트랜잭션이 종료되면, Purge Thread라는 백그라운드 스레드에 의해 주기적으로 디스크 파일에서 삭제된다. (이 내용은 뒤의 격리수준에서 자세히 다루겠다)
* Redo는 Checkpoint 시점에 더티 페이지가 디스크의 데이터 파일에 안전하게 기록되면, 더 이상 Redo 로그는 필요가 없으므로 삭제된다.
트랜잭션이 길어지면, Undo에서 관리하는 예전 데이터가 조회가 될 수 있기에 삭제되지 못하고 오랫동안 관리돼야 하며, 자연히 Undo 영역이 저장되는 시스템 테이블스페이스의 공간이 많이 늘어나는 상황이 발생할 수도 있다. 따라서 트랜잭션이 시작됐다면, 가능한 빨리 Rollback이나 Commit을 통해 트랜잭션을 완료하는 것이 좋다. 즉, 프로그램 코드에서 트랜잭션 범위를 가능한 최소화하라는 의미다.
ex) db 커넥션은 개수가 제한적이기에 최대한 원자성이 보장되어야 하는 구간에만 트랜잭션을 설정하자.
ex) 네트워크 통해 원격 서버와 통신하는 작업은 트랜잭션 내에서 제거하자. 네트워크 상에 문제가 발생하면 자칫 DBMS 서버가 높은 부하 상태가 되기 쉽다.
Commit이 되면, InnoDB는 더 이상 변경 작업 없이 버퍼풀의 상태를 영구적인 상태로 만든다. 즉, Redo 로그를 디스크의 로그 파일로 저장해 서버가 꺼져도 재시작시 Redo 로그를 읽어 버퍼풀을 복구할 수 있게된다(Write Ahead Logging).
Rollback이 되면, InnoDB는 Undo 영역에 있는 백업된 데이터를 InnoDB 버퍼 풀로 다시 복구하고, 언두 영역의 내용을 삭제해버린다.
3. MVCC(Multi Version Concurrency Control)
하나의 레코드에 대해 여러 개의 버전이 동시에 관리하여 잠금을 사용하지 않는 일관된 읽기를 제공한다.
위에서 소개된 Undo 로그를 통해 트랜잭션마다 변경 내용을 기록하고 있고, 이것이 여러개의 버전을 의미한다.
Undo 로그 형식은 다음과 같다.
1. [start_transaction, T]: Transaction T가 실행을 시작했음
2. [write_item, T, x, old_value, new_value]: Transaction T가 db item x의 값을 old_value에서 new_value로 변경했음
3. [read_item, T, x]: Transaction T가 db item x를 read했음
4. [commit, T]: Transaction T가 성공적으로 완료되었음. 그 결과가 db에 commit(영구 기록)될 수 있음.
5. [abort, T]: Transaction T가 중단되었음
Undo 로그를 통해 Commit 혹은 Rollback 이전 다른 사용자가 작업중인 데이터를 조회한다면, 격리수준에 따라 다른 결과를 반환할 수 있게 됐다.
- read_uncommited인 경우에는 버퍼풀이 가지고 있는 변경된 데이터를 읽어서 반환한다.
- read_commited나 그 이상의 격리 수준인 repeatable_read, serializable인 경우에는 변경되기 이전의 내용을 보관하고 있는 Undo 영역의 데이터를 반환한다.
(격리수준에 대해서는 아래에서 더 자세히 다루겠다)
이처럼 InnoDB 스토리지 엔진은 MVCC 기술을 이용해 잠금을 걸지 않고, 읽기 작업을 수행한다. 그렇기에 격리 수준이 serializable인 경우를 제외하곤 다른 트랜잭션의 변경 작업과 관계없이 항상 잠금을 대기하지 않고 바로 읽을 수 있다. serializable은 SELECT 쿼리를 S-Lock으로 바꾸어 읽기와 쓰기가 동시에 일어나지 않도록 한다.
4. 격리 수준
Isolation level이란? 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.
좌측은 격리수준이며, 우측은 부정합의 문제이다.
* 격리 수준에서 밑으로 갈수록 각 트랜잭션 간의 데이터 격리(고립) 정도가 높아지며, 동시 처리 성능도 떨어진다.
| Dirty Read | Non-Repeatable Read | Phantom Read | |
| READ UNCOMMITTED | O(발생) | O | O |
| READ COMMITTED | X(없음) | O | O |
| REPEATABLE READ | X | X | O(다만, InnoDB는 갭락으로 없음) |
| SEARIALIZABLE | X | X | X |
*Dirty Read: 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있는 문제
*Non-Repeatable Read: 같은 데이터를 여러 번 읽을 때, 트랜잭션 실행 중간에 값이 변경되는 문제
*Phantom Read: 반복 조회할 때, 중간에 다른 트랜잭션이 새로운 데이터를 추가하여 결과가 달라지는 문제
Oracle DBMS에서는 READ COMMITTED 수준을 많이 사용하며, MySQL에서는 REPEATABLE READ를 주로 사용한다.
각각의 격리 수준에 대해 자세히 알아보자.
1) READ UNCOMMITTED : 각 트랜잭션에서의 변경 내용이 Commit이나 Rollback여부에 상관없이 다른 트랜잭션에서 보인다. 그래서 Rollback이 될 내용을 정상적인 데이터라고 생각하고 처리하여 개발자와 사용자가 혼란스러울 수 있다.

2) READ COMMITTED : MVCC를 활용해 Commit이 완료된 데이터만 조회한다.

다시 한번 강조하지만, 쿼리가 전달되면 버퍼풀은 새로운 값으로 업데이트가 되는 동시에 이전 데이터는 Undo 로그에 기록된다. 그래서 READ COMMITTED에서는 버퍼 풀이 아닌 Undo 영역에서 데이터를 가져오게 되고, Commit되면 그때부터 Undo가 아니라 변경된 값이 반영된 버퍼 풀 혹은 디스크에서 가져온다.
READ UNCOMMITTED에서 발생한 Dirty Read 문제는 사라졌지만, 작업 도중에 다른 트랜잭션이 Commit된다면, 한 트랜잭션 안에서 같은 쿼리로 조회 시, 반환되는 데이터가 달라질 수 있다는 문제가 존재한다. (아래 사진) 그러면 SQL 문장이 어떤 결과를 가져오게 되는지 정확히 예측하기가 어려워진다.

3) REPEATABLE READ : MVCC를 활용해 트랜잭션 내부에서 반복해서 조회해도 항상 같은 결과를 보장한다. 이때부터 SELECT 쿼리도 트랜잭션 범위 내에서만 작동한다.
*바이너리 로그를 가진 MySQL 서버에서는 최소 repeatable read 격리 수준을 사용해야 한다.
read uncommitted와 똑같이 MVCC를 사용하지만, 언두 영역에 백업된 레코드의 여러 버전 중 몇 번째 이전 버전까지 찾아 들어가느냐가 다르다. 모든 트랜잭션은 순차적으로 증가하는 고유한 번호를 가지며(BEGIN 시 번호 부여), 위에서 다뤘던 것 처럼 Undo 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있다. 이를 이용해 자신의 트랜잭션 번호보다 작은 트랜잭션 번호에서 변경한 것만 본다.

하지만, repeatable_read에도 부정합이 발생할 수 있다. 새롭게 데이터가 추가될 때에는 Undo 로그에 이전 상태가 기록되지 않는다(Undo는 레코드 단위로 관리). 이렇게 데이터 삽입으로 인한 부정합을 해결하기 위해선 갭락이나 넥스트 키 락을 사용하면 된다.

4) SERIALIZABLE : 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야만 하며, 동시에 다른 트랜잭션은 잠금이 걸린 레코드를 변경하지 못하게 되어 INSERT의 경우에도 항상 동일한 결과를 얻을 수 있다.
하지만, InnoDB 스토리지 엔진에서는 갭락과 넥스트 키 락 덕분에 repeatable read 격리 수준에서도 이미 phantom read가 발생하지 않아 굳이 serializable을 사용할 필요는 없다.
락과 관련된 내용은 아래 글을 참고하면 좋을 것 같다.
https://persi0815.tistory.com/172
'공부 > RDBMS' 카테고리의 다른 글
| [DB] MySQL 8.0 잠금 (1) | 2026.01.14 |
|---|---|
| [DB] MYSQL 엔진 아키텍처 (0) | 2026.01.12 |
| [DB] MySQL 락 걸어보고 분석해보기! (0) | 2025.08.23 |
| [Cache] 캐시 전략 5가지 (0) | 2025.05.14 |
| [DB/MYSQL] SQL 고득점 Kit - SELECT문(Lv.3~5) (0) | 2025.02.21 |