어떤 개념일까?
트랜잭션 락은 여러 트랜잭션이 같은 데이터에 동시 접근할 때, 충돌이 데이터를 망가뜨리지 못하도록 접근 순서를 제어하는 메커니즘이다.
보통 락은 2개의 기준으로 분류한다.
1. 무엇을, 어떻게 잠그는지에 따른 기준 (DB 엔진 내부의 락 모드와 대상)
모드: S(Shared) 공유락 / X(Exclusive) 배타락테이블 레벨 의도락: 의도 공유락(IS), 의도 배타락(IX)행/범위 단위: 레코드 락 / 갭 락 / 넥스트키락 / 인서트 인텐션 락
2. 충돌을 어떻게 해결하냐에 따른 기준
비관적 락Pessimistic Lock: 충돌이 자주 일어나서 미리 잠근다.낙관적 락Optimistic Lock: 충돌이 드물기 때문에 안 잠그고 커밋 시점에 검증한다.
즉, 1번은 DB가 자동으로 하는 저수준 락이고, 2는 애플리케이션/개발자가 선택하는 전략이다.
어떤 문제를 해결하려고 나왔을까? 왜 사용 할까?
동시성 이상 현상을 막기 위해서 필요하다. 락이 없다고 생각을 해보면 동시 접근 시에 다음과 같은 문제들이 발생하게 된다.
Lost Update
A와 B가 같은 행일 읽고 각자 수정 후 저장한다면, 나중에 쓴 쪽이 먼저 쓴 변경을 덮어씀
Dirty Read
아직 커밋 안 된 변경을 다른 트랜잭션이 읽음
Non-repeatable Read
같은 행을 두번 읽었는데 값이 달라짐 사이에 다른 트랜잭션이 그 행을 수정 커밋함
행의 값이 바뀌는 것
Phantom Read
같은 쿼리를 두번 실행했는데 결과가 달라짐
같은 조건 범위 쿼리를 두번 했는데 행의 개수가 달라짐 사이에 다른 트랜잭션이 조건에 맞는 행을 삽입 삭제를 함
행의 집합이 바뀌는 것
이러한 현상들이 어느정도까지 허용할지 즉, 격리 수준이고, 락과 MVCC는 그 격리 수준을 구현하는 수단이다. 즉, 락은 격리 수준의 목표를 달성하기 위한 도구이다.
격리 수준 매트릭스
이상 현상을 “어디까지 허용할지"를 4단계로 정의한 게 격리 수준이다. 위로 갈수록 안전하지만 동시성은 떨어진다.
| 격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | 가능 | 가능 | 가능 |
| READ COMMITTED | 방지 | 가능 | 가능 |
| REPEATABLE READ | 방지 | 방지 | 가능 |
| SERIALIZABLE | 방지 | 방지 | 방지 |
락이 왜 필요할까
같은 격리 수준이라도 구현 수단에 따라 갈린다.
- 락 기반 : 충돌 가능 지점을 미리 잠가서 직렬화
- MVCC 기반 : 버전을 여러 개 유지해 읽기는 락 없이 일관된 스냅샷을 보게 한다.
어떻게 동작하나?
1. InnoDB 내부 락 모드
공유락 S락
읽기용 락으로, 다른 트랜잭션도 같은 행에 S락은 동시 획득이 가능하다. X는 불가능
배타락 X락
쓰기용 락으로, 다른 트랜잭션이 같은 행에 S/X 어떤 락도 갖지 못한다.
의도락 IS / IX
테이블 레벨, 행락과 테이블락의 공존을 위한 장치로써 행에 S/X 락을 걸기 전에 테이블에 먼저 IS/IX를 건다.
누군가 테이블 안의 행을 잠그고 있다는 사실을 테이블 레벨에 표시해 두는 목적으로 사용되고,어떤 트랜잭션이 테이블 전체 락을 요청할 때, 모든 행을 일일이 확인하지 않고 의도락만 보고 충돌 판단이 가능하여 호율이 향상된다.
IS/IX락 끼리는 서로 충돌하지 않는다
레코드 / 갭 / 넥스트키 락
InnoDB에서 행 레벨 락은 인덱스 레코드락이다.
- 레코드 락: 인덱스 레코드 하나를 잠근다.
- 갭락: 인덱스 레코드 사이의 빈 공간을 잠근다. 그 틈에 새행이 삽입되는 것을 막는것이다. (팬텀 방지) 서로 다른 트랜잭션이 같은 갭에 갭 락을 동시에 가질 수 있고 S/X 구분도 의미가 없다.
- 넥스트키 락: 레코드 + 그 레코드 앞쪽 갭의 갭락이다. 그 REPEATABLE READ에서 기본으로 쓰이며 팬텀 리드를 막는 것이 핵심이다.
- 인서트 인텐션 락: 삽입 직접에 거는 갭 락의 일종으로, 같은 갭이라도 서로 다른 위치에 넣는 삽입끼리는 대기하지 않도록 동시 삽입 성능을 높인다.
2. 충돌을 어떻게 해결하냐에 따른 기준
비관적 락 Pessimistic
데이터를 읽는/수정하는 시점에 먼저 잠근다. 트랜잭션이 끝날 때까지 다른 트랜잭션을 막는다.
SQL
SELECT ... FOR UPDATE -> X락
SELECT ... FOR SHARE -> S락
JPA
@Lock(PESSIMISTIC_WRITE)
낙관적 락 Optimistic
읽는 동안 락을 걸지 않는다. 커밋 직전에 내가 읽은 뒤로 누가 이걸 바꿨나?를 검증한다.
데이터 조회 할 때 version 값을 함께 가져온다. 데이터 수정한 후 커밋하여 UPDATE 쿼리 실행할 때, 조회 당시 version과 DB version이 일치하는지 비교한다.
- 업데이트가 성공되면 version 값이 자동으로 1 증가한다.
- version 컬럼을 통해 UPDATE 시
WHERE id=? AND version=?
영향 받은 행이 0이면 다른 트랜잭션이 이미 수정을 한것으로 간주하여 충돌로 판단하여, 롤백 후에 재시도를 한다.
언제 쓰고, 언제 안 쓰나?
비관적 락
- 쓰기 충돌이 자주 일어나는 hot row 로 재고 차감, 잔액 갱신, 좌석/예약 선점
- 재시도 비용이 큰 작업
- 충돌 시 데이터 정합성이 중요한 경우
낙관적 락
- 읽기 위주 + 충돌이 드문 환경
- 재시도가 허용되는 작업
남에게 설명한다면 어떻게 설명할 것인가?
추가 궁금한 질문들
- MVCC와 락은 무슨 관계인가?
MVCC는 “버전을 여러 개 유지해 읽기는 락 없이 스냅샷을 보게 하는” 방식인데,
InnoDB에서 일반 SELECT(스냅샷 읽기)와 FOR UPDATE(잠금 읽기)는 어떻게 다르게 동작하나? → MySQL InnoDB vs PostgreSQL MVCC 비교로 이어짐
- 갭 락이 데드락을 유발하는 시나리오는?
같은 갭에 여러 세션이 INSERT할 때 insert intention lock과 갭 락이 어떻게 얽혀 데드락이 나나?
- 격리 수준별로 거는 락이 어떻게 달라지나?
READ COMMITTED는 갭 락을 거의 안 걸고, REPEATABLE READ는 넥스트키 락을 거는 이유는?
- 낙관적 락의 재시도는 어디서 처리?
애플리케이션 레이어 재시도(@Retryable) vs 트랜잭션 재시작의 경계는?
- **
SELECT ... FOR UPDATE**가 인덱스를 안 타면?
잠금 범위가 의도보다 넓어지는(테이블 전체 스캔 → 전 행 락) 문제는 어떻게 진단하나?
- PostgreSQL은 어떻게 다른가?
PostgreSQL의 row-level lock 모드 (FOR UPDATE / FOR NO KEY UPDATE / FOR SHARE / FOR KEY SHARE)와 InnoDB 모델의 차이는?