Transaction & Lock
Transaction & Lock에 대해서 적어봤습니다.
주제 선정 이유
프로젝트를 진행하면서 @Transactional을 사용하다 보면 트랜잭션 처리는 자연스럽게 적용되지만, 동시성 문제에
대해서는 데이터베이스가 알아서 해결해줄 것이라고 생각하고 넘어가는 경우가 많습니다.
특히 같은 데이터를 조회했는데도 시점에 따라 결과가 달라지는 현상이나, REPEATABLE READ와 같은 격리 수준이 실제로 무엇을 보장하는지, 그리고 그 보장이 Lock을 통해 이루어지는지 MVCC를 통해 이루어지는지에 대해서는 명확히
설명하기 어렵다는 점을 깨닫게 되었고 이러한 경험을 통해 트랜잭션 격리 수준과 락, 그리고 MVCC는 단순한 설정
옵션이 아니라 데이터베이스가 동시성을 처리하기 위한 핵심 구조라는 사실을 인식하게 되었습니다.
그래서 이번 글에서는 트랜잭션 격리 수준과 Lock, MVCC가 어떤 방식으로 동작하며, 이들이 데이터 일관성과 동시성에 어떤 영향을 미치는지를 중심으로 살펴보고, 이를 통해 데이터베이스의 동시성 처리 구조를 정리해보고자 합니다.
트랜잭션 격리 수준이 뭘까?
트랜잭션 격리 수준 (Isolation Level)은 여러 트랜잭션이 동시에 실행될 때 서로의 작업을 얼마나 안 보이게 할지를
정하는 규칙인데 격리 수준이 낮으면 성능은 좋지만 데이터가 흔들리고, 높으면 정합성은 좋아지지만 락 경합 / 대기가
늘어 성능이 떨어질 수 있습니다.
READ UNCOMMITTED- 커밋 안 된 데이터도 읽음 → 모든 이상 현상 발생 가능
READ COMMITTED- 커밋된 데이터만 읽음 →
Dirty Read방지 - 하지만 같은 트랜잭션 내에서도 조회 시점마다 결과가 달라질 수 있음
- 커밋된 데이터만 읽음 →
REPEATABLE READ- 트랜잭션 시작 시점 스냅샷 기준으로 일관된 조회 보장
DB구현에 따라Phantom Read처리 방식이 달라짐MySQL InnoDB는RR에서도Gap/Next-KeyLock으로Phantom을 강하게 막는 편
SERIALIZABLE- 순서대로 실행한 것처럼 결과 보장
- 가장 안전하지만 충돌 / 대기 / 롤백이 늘어 성능 비용이 큼
격리 수준이 필요한 이유는 대표적인 4가지 읽기 이상 현상 때문입니다.
이상 현상
Dirty Read
- 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상
- 롤백되면 존재하지 않았던 데이터를 읽고 로직이 진행될 수 있음
Non-Repeatable Read
- 같은 트랜잭션에서 같은
Row를 두 번 읽었는데 값이 달라지는 현상 - 중간에 다른 트랜잭션이
Update + Commit
Phantom Read
- 같은 조건으로 조회했는데 결과 집합이 달라지는 현상
- 중간에
Insert/Delete가 발생
Write Skew
- 두 트랜잭션이 같은 기준으로 판단한 뒤 서로 다른
Row를 업데이트해서 규칙이 깨지는 현상 - 스냅샷 기반 (
MVCC)에서도 발생할 수 있는 직렬화 이상
Lock은 뭘까?
격리 수준이 논리적인 규칙이라면, Lock은 그 규칙을 지키기 위한 물리적인 제어 장치입니다.
S-Lock / X-Lock
- 공유 락(
S-Lock,Shared Lock)- 데이터를 읽기 위한 락
- 여러 트랜잭션이 동시에 가질 수 있음 (
S↔S가능)
- 배타 락(
X-Lock,Exclusive Lock)- 데이터를 수정하기 위한 락
- 한 트랜잭션만 가질 수 있음 (
S↔X,X↔X불가)
위에 사진을 설명하면 다음과 같습니다.
- 트랜잭션 1이 리소스 1에
X-Lock을 획득한다.- 해당 리소스는 다른 트랜잭션이 읽거나 수정할 수 없다.
- 트랜잭션 2가 리소스 2에
S-Lock을 획득한다.- 다른 트랜잭션도 읽기는 가능하지만, 수정은 불가능하다.
- 트랜잭션 2가 리소스 1에
S-Lock을 요청한다.- 하지만 리소스 1은 이미 트랜잭션 1이
X-Lock을 보유 중 S-Lock과X-Lock은 호환되지 않으므로- 트랜잭션 2는 대기 상태에 들어간다.
- 하지만 리소스 1은 이미 트랜잭션 1이
- 트랜잭션 1이 리소스 2에
X-Lock을 요청한다.- 리소스 2는 트랜잭션 2가
S-Lock을 보유 중 S-Lock과X-Lock은 호환되지 않으므로- 트랜잭션 1도 대기 상태에 들어간다.
- 리소스 2는 트랜잭션 2가
대기
대기란 다른 트랜잭션이 락을 해제할 때까지 해당 트랜잭션의 실행이 일시적으로 멈춰 있는 상태를 의미합니다.
- 트랜잭션 1은 트랜잭션 2의
S-Lock해제를 기다리고 - 트랜잭션 2는 트랜잭션 1의
X-Lock해제를 기다립니다
서로가 서로를 기다리는 구조가 되며, 이를 Deadlock이라고 합니다.
데드락이 발생하는 이유
이 그림은 전형적인 순환 대기 (Circular Wait)구조입니다.
T1→R1점유,R2대기T2→R2점유,R1대기
누군가 먼저 락을 포기하지 않으면 영원히 진행되지 않기 때문에 대부분의 DB는 Wait-for Graph 같은 내부
알고리즘으로 데드락을 감지하고, 한 트랜잭션을 강제로 롤백시켜 문제를 해결합니다.
데드락 감지 방법
대부분의 DBMS는 데드락을 예방하기보다는 감지하고 강제로 해소하는 전략을 사용합니다.
Wait-for Graph 방식
가장 대표적인 기반 감지인데 각 트랜잭션을 하나의 노드로 보고 A가 B를 기다린다는 관계를 간선으로 표현합니다.
T1이T2가 가진 락을 기다리면 →T1→T2T2가T1이 가진 락을 기다리면 →T2→T1
이렇게 연결된 그래프에 순환이 생기면 그 순간이 바로 DeadLock이며 DB는 주기적으로 이 그래프를 탐색하여
사이클이 발견되면 즉시 개입합니다.
Victim Selection 방식
DeadLock이 감지되면 DB는 얽혀 있는 트랜잭션 중 하나를 강제로 롤백시킵니다.
- 변경한 데이터가 적은 트랜잭션
- 롤백 비용이 적은 트랜잭션
- 우선순위가 낮은 트랜잭션
이 트랜잭션을 희생자 (Victim)라고 하며 희생자가 롤백되면 Lock이 해제되고 나머지 트랜잭션은 정상적으로
진행됩니다.
Timeout-Based 방식
일부 DB는 단순하게 일정 시간 이상 대기하면 강제 종료하는 방식도 사용하지만 이 방식은 진짜 DeadLock이 아닌 단순 지연까지 잘못 감지할 수 있기 때문에 보통은 보조 수단으로 사용됩니다.
그래서 어떻게 쓰라고?
격리 수준은 무조건 높인다고 좋은게 아니다
SERIALIZABLE이 가장 안전해 보이지만, 동시성이 높은 서비스에서는 충돌과 롤백이 급격히 증가할 수 있으며 대부분의 웹 서비스에서는 기본값 (READ COMMITTED 또는 REPEATABLE READ)을 사용하고 정말 정합성이 중요한 구간에서만
명시적 락을 사용하는 전략이 현실적인 선택이라고 생각합니다.
리소스 접근 순서를 일관되게
데드락의 가장 큰 원인은 트랜잭션마다 리소스 접근 순서가 다른 것이기 때문에 예를 들어서 항상
A 테이블 → B 테이블 순서로 접근하도록 통일하면 순환 대기 가능성이 크게 줄어듭니다.
인덱스를 통해 락 범위 줄이기
인덱스가 없으면 DB는 더 많은 Row를 스캔하고, 그 과정에서 더 많은 락이 발생합니다.
마무리
트랜잭션 격리 수준과 락을 정리하면서 느낀 핵심을 다시 정리해보면 다음과 같습니다.
- 격리 수준은 단순한 설정 옵션이 아니라, 동시성 환경에서 어떤 이상 현상을 허용하고 어디까지 정합성을
보장 할 것인지 결정하는 설계 선택이라는 점을 이해하게 되었습니다. Lock은 내부 구현 디테일이 아니라, 그 격리 수준을 실제로 지키기 위한 물리적인 제어 장치이며,S-Lock과
X-Lock의 호환성 구조가 시스템의 동작 방식 전체를 좌우한다는 사실을 확인하게 되었습니다.DeadLock은 예외적인 오류가 아니라 동시성이 높아질수록 자연스럽게 발생할 수 있는 구조적 현상이며, 이를
감지하고 희생자를 선택해 해소하는DB의 메커니즘까지 이해해야 비로소 트랜잭션을 제대로 설계할 수 있다는
점을 체감하게 되었습니다.




