일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- deque
- 큐
- 리소스모니터링
- Java
- map
- string
- dfs
- alter
- scanner
- Union-find
- set
- date
- NIO
- math
- 힙덤프
- Properties
- sql
- GC로그수집
- javascript
- 스프링부트
- 스택
- List
- CSS
- BFS
- Calendar
- JPA
- html
- union_find
- spring boot
- priority_queue
- Today
- Total
매일 조금씩
트랜잭션 격리 수준 (feat. 비관적 락, 낙관적 락, MVCC) 본문
트랜잭션 격리 수준이란?
데이터베이스에서 여러 트랜잭션이 동시에 실행될 때,
각각의 트랜잭션이 다른 트랜잭션으로부터 얼마나 독립적으로 동작할 수 있는지를 정의하는 기준이다.
이를 통해 동시성 문제(Concurrency Issues)를 제어하고 데이터의 일관성을 유지한다.
주요 동시성 문제
- Dirty Read (더티 리드)
다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 현상.
업데이트, 삭제에 대해 발생하고, 행 락으로 방지할 수 있다. Read Commited 이상에선 방지된다. - Non-Repeatable Read (반복 불가능한 읽기)
한 트랜잭션 내에서 같은 데이터를 두 번 읽을 때 값이 다른 현상. (다른 트랜잭션이 데이터를 수정한 경우) - Phantom Read (팬텀 리드)
한 트랜잭션이 동일한 쿼리를 두 번 실행할 때,
다른 트랜잭션의 삽입/삭제로 인해 데이터가 유령(Phantom)처럼 있었는데 없었어요 하며
조회 결과의 레코드 수나 내용이 달라지는 현상.
트랜잭션 격리 수준 종류 (MySQL 기준)
- Read Uncommitted (읽기 미완료 허용) - MVCC 사용 X
- 커밋되지 않은 데이터를 읽을 수 있음.
- 가장 낮은 격리 수준, 높은 동시성 제공.
- 문제점: Dirty Read, Non-Repeatable Read, Phantom Read 발생 가능.
- Read Committed (읽기 완료 허용) - MVCC 적극 사용
- 커밋된 데이터만 읽을 수 있음.
- Dirty Read 방지.
- 문제점: Non-Repeatable Read, Phantom Read 발생 가능.
- Repeatable Read (반복 가능한 읽기) - MVCC 적극 사용
- 트랜잭션 내에서 동일한 데이터를 반복적으로 읽을 때 항상 같은 결과를 보장.
- Dirty Read와 Non-Repeatable Read 방지.
- 문제점: Phantom Read 발생 가능. (MySQL에선 발생 안함. 아래에서 설명)
- Serializable (직렬화 가능) - MVCC 부분 사용
- 가장 높은 격리 수준.
- 트랜잭션을 순차적으로 실행하는 것처럼 동작.
- Dirty Read, Non-Repeatable Read, Phantom Read 모두 방지.
- 단점: 성능 저하, 동시성 감소.
격리 수준 선택 기준
- 동시성이 중요한 경우: 낮은 수준(Read Uncommitted, Read Committed).
- 데이터 일관성이 중요한 경우: 높은 수준(Repeatable Read, Serializable).
트랜잭션 격리 수준 설정 (SQL 예시)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DB별 기본 격리 수준
- MySQL: Repeatable Read.
- Oracle: Read Committed.
- PostgreSQL: Read Committed.
트랜잭션 격리 수준은 총 4단계일까?
=> DB 마다 다르다.
위에 설명한 격리 수준은 MySQL 기준이고, 4단계이다.
Oracle, Tibero는 총 3단계를 제공한다.
dirty read를 엄격히 금지하여 read uncommited는 아예 없다.
MySQL은 기본으로 Repeatable Read 인데, Phantom Read가 발생하지 않는다?
=> 그렇다. MySQL의 InnoDB 스토리지 엔진은 MVCC(Multi-Version Concurrency Control)를 사용하기 때문에, Repeatable Read 격리 수준에서도 Phantom Read가 발생하지 않는다.
MySQL에서 Phantom Read 방지 메커니즘
- MVCC
MVCC란, 하나의 데이터를 여러 버전으로 관리하면서 트랜잭션마다 자기만의 스냅샷(버전)을 읽게 하는 방식.
MySQL의 InnoDB는 MVCC를 통해 트랜잭션이 시작될 때
undo log 기반으로 해당 트랜잭션 ID가 볼수 있는 데이저 버전 목록을 기억하여 스냅샷(Snapshot)을 생성하고,
트랜잭션 동안 동일한 스냅샷을 참조한다.
이를 통해 반복 가능한 읽기(Repeatable Read)를 보장한다.- READ COMMITTED 는 쿼리마다 새로운 스냅샷
- REAPEATABLE READ 는 트랜잭션 시작 시점 스냅샷 유지
- Next-Key Locking
MySQL은 Repeatable Read에서 레코드 락(Record Lock)과 갭 락(Gap Lock)을 조합한 Next-Key Locking을 사용.- 레코드 락: 특정 행(row)을 잠금.
- 갭 락: 특정 행 주변의 인덱스 범위(gap)를 잠금.
Phantom Read 문제 또한 발생하지 않도록 막을 수 있다.
MySQL의 REPEATABLE READ는 MVCC + Next-Key Locking(갭 락) 덕분에
phantom read 없이 높은 격리성을 제공하며,
일부 상황에서는 다른 DB의 SERIALIZABLE과 유사하게 동작한다고 볼 수 있다.
Next-Key Locking과 트랜잭션 ID(Transaction ID)의 쓰임과 관계는?
- Next-Key Locking :
Next-Key Locking은 동시성 제어와 데이터 무결성 보장을 위해 MySQL InnoDB가 사용하는 잠금 메커니즘.- 목적: 트랜잭션 간 충돌을 방지하고 Phantom Read를 막기 위해 인덱스의 특정 범위와 해당 데이터를 잠금.
- 작동 방식:
- 레코드 락 (Record Lock): 행 자체에 락을 설정.(SELECT ... FOR UPDATE와 같은 쿼리에서 사용)
- 갭 락 (Gap Lock): 행 사이의 "갭"에 락을 설정. (id = 10, 20 이 있으면 10부터 20까지 갭에 락이 걸려서 id = 15를 insert 하려고 해도 안됨)
- Next-Key Lock: 레코드 락 + 갭 락을 결합하여 사용.
- 트랜잭션 ID (Transaction ID) :
트랜잭션 ID는 각 트랜잭션을 고유하게 식별하는 ID로, InnoDB에서 MVCC를 구현하는 데 사용.- 목적: 트랜잭션 간 데이터의 일관성을 보장하고, 어떤 트랜잭션이 어떤 데이터를 읽거나 변경했는지를 추적.
- 작동 방식:
- 트랜잭션이 시작될 때마다 고유한 트랜잭션 ID가 할당된다.
- MVCC는 이 트랜잭션 ID를 사용해 데이터의 버전을 관리.
- 특정 트랜잭션이 볼 수 있는 데이터의 범위를 결정.
그럼 낙관적 락, 비관적 락은 왜 사용할까?
트랜잭션 격리 수준은 주로 읽기 시점의 일관성을 보장하는 기능이다.
읽기뿐 아니라 동시 수정에 대한 충돌 방지도 필요하다.
예를 들어, 두 트랜잭션이 동시에 같은 데이터를 읽고, 각자 수정한 뒤 커밋하려 하면 마지막에 덮어쓰기가 발생하여,
좀더빨리 업데이트한 데이터가 유실될수 있다.
이런 쓰기 충돌은 트랜잭션 격리 수준만으로는 완벽히 제어할 수 없다.
SERIALIZABLE로 예방할 수 있지만, SERIALIZABLE은 동일한 데이터에 대해 읽고 쓰려는 트랜잭션이 동시에 존재하면 한쪽 트랜잭션을 롤백 처리해버려서 강제 실패시킨다. 그래서 성능 저하와 재시도하는 예외 처리를 개발자가 추가해야하기 때문에 이런 부담으로 실무에선 잘 쓰지 않는다.
그래서 등장하는 것이 바로 비관적 락과 낙관적 락이다.
🔹 비관적 락(Pessimistic Lock)
"다른 트랜잭션이 데이터를 건드릴 거라 생각하고 선제적으로 락을 건다."
- 대표 예: SELECT ... FOR UPDATE
- 데이터 조회와 동시에 행에 락을 걸어, 다른 트랜잭션의 접근을 차단
- 확실한 충돌 방지 가능, 하지만 락 경쟁 발생 시 성능 저하 우려
- @Lock으로 가능(PESSIMISTIC_READ - 공유락(읽기만 허용), PESSIMISTIC_WRITE - 배타 락(쓰기/읽기 모두 막음))
🔹 낙관적 락(Optimistic Lock)
"충돌이 드물 거라 믿고, 변경 시점에만 검증한다."
- 대표 예: JPA의 @Version
- 데이터를 읽을 때는 자유롭게 읽고,
저장 시점에 (업데이트하려는 대상 데이터와 업데이트용 엔티티의)버전 번호를 비교해서 충돌을 감지 - 성능에 유리하지만, 충돌 발생 시 예외 발생 후 재시도 필요
그럼 분산락은 왜 필요한가?
비관적/낙관적 락은 충돌 시 대부분 ‘폴링 기반 재시도’로 처리된다.
(비관적 락은 DB가 직접 락을 걸고 기다리는 '진짜 대기' 구조이고,
낙관적 락과 SERIALIZABLE은 작업 시도 후 예외가 발생하면 개발자가 수동으로 재시도 로직을 구현하는 구조)
이는 성공할 때까지 계속 시도하는 구조이기 때문에, 결과적으로 CPU, 스레드, DB 부하 등 리소스 낭비가 발생한다.
특히 MSA 구조에서는 요청량이 많고, 서비스 간 호출이 겹치기 때문에,
재시도 로직이 성능 저하는 물론, 장애를 연쇄적으로 전파시킬 위험도 크다.
또한, 여러 자원에 동시에 락을 거는 상황에서는 서로 교차 락을 걸다가 데드락(Deadlock)이 발생할 가능성도 존재한다.
데드락은 트랜잭션이 영원히 대기 상태에 빠지는 치명적인 문제로, 폴링 방식의 재시도 구조에서는 감지와 해소가 어렵다.
✅ 이러한 문제를 해결하기 위해
전역에서 락을 일관되게 관리할 수 있는 '분산 락(Distributed Lock)'이 필요하다.
- Redis나 ZooKeeper 기반 분산 락은
여러 서버나 인스턴스가 동일한 자원을 동시에 조작하지 못하게 막아주고, - TTL(Time to Live)과 try-lock 구조를 통해
락이 오래 유지되어 생기는 데드락도 예방할 수 있다. - 또한, 락 획득 순서를 강제하거나
재시도 횟수와 대기 시간(Backoff)을 조절함으로써
분산 환경에서도 안정적인 자원 접근 제어가 가능하다.