[Daily morning study] 데이터베이스 락(Lock)의 종류와 동작 방식
#daily morning study
락(Lock)이란?
데이터베이스는 여러 트랜잭션이 동시에 같은 데이터에 접근할 수 있다. 이때 데이터 일관성을 보장하기 위해 락(Lock) 을 사용한다. 락은 특정 자원에 대한 접근을 제어하는 메커니즘이다.
락은 크게 두 가지 관점에서 분류할 수 있다: 무엇을 잠그느냐(범위) 와 어떻게 잠그느냐(종류).
락의 범위: 어느 수준을 잠그느냐
| 범위 | 설명 |
|---|---|
| 테이블 락(Table Lock) | 테이블 전체를 잠금. 영향 범위가 크지만 오버헤드가 낮음 |
| 페이지 락(Page Lock) | 데이터 페이지 단위로 잠금. 일부 DB에서만 지원 |
| 로우 락(Row Lock) | 특정 행(row)만 잠금. 동시성이 높지만 오버헤드가 큼 |
| 컬럼 락(Column Lock) | 특정 컬럼만 잠금. 거의 사용되지 않음 |
대부분의 OLTP 환경에서는 로우 락이 기본이다. MySQL InnoDB, PostgreSQL 모두 기본적으로 행 단위 잠금을 사용한다.
락의 종류: 어떻게 잠그느냐
공유 락(Shared Lock, S-Lock)
읽기 작업에 사용하는 락이다. 공유 락이 걸린 자원은 다른 트랜잭션도 읽기는 가능하지만, 쓰기는 불가능하다.
트랜잭션 A: 공유 락 획득 → SELECT
트랜잭션 B: 공유 락 획득 → SELECT ← 가능 (공유 가능)
트랜잭션 C: 배타 락 시도 → SELECT FOR UPDATE ← 대기 (블로킹)
SQL에서 명시적으로 공유 락을 걸 수 있다:
-- PostgreSQL
SELECT * FROM orders WHERE id = 1 FOR SHARE;
-- MySQL
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;
배타 락(Exclusive Lock, X-Lock)
쓰기 작업에 사용하는 락이다. 배타 락이 걸린 자원은 다른 트랜잭션이 읽기도, 쓰기도 불가능하다.
트랜잭션 A: 배타 락 획득 → UPDATE
트랜잭션 B: 공유 락 시도 → SELECT ← 대기
트랜잭션 C: 배타 락 시도 → UPDATE ← 대기
INSERT, UPDATE, DELETE는 자동으로 배타 락을 획득한다. 명시적으로도 걸 수 있다:
-- PostgreSQL
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- MySQL
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
락 호환성 매트릭스
| 공유 락(S) | 배타 락(X) | |
|---|---|---|
| 공유 락(S) | 호환 O | 호환 X |
| 배타 락(X) | 호환 X | 호환 X |
의도 락(Intent Lock)
테이블 락과 로우 락이 함께 사용될 때 효율적인 호환성 검사를 위한 락이다. 상위 계층(테이블)에 “하위 계층(행)에 락이 걸릴 것”이라는 의도를 미리 표시한다.
| 의도 락 종류 | 설명 |
|---|---|
| IS (Intent Shared) | 하위 자원에 공유 락을 걸 의도 |
| IX (Intent Exclusive) | 하위 자원에 배타 락을 걸 의도 |
| SIX (Shared + Intent Exclusive) | 현재 자원에는 공유 락, 하위에는 배타 락 의도 |
의도 락 덕분에 DB는 전체 행을 스캔하지 않고 테이블 수준에서 충돌 여부를 빠르게 판단할 수 있다.
낙관적 락 vs 비관적 락
락을 언제 획득하느냐는 전략의 문제다.
비관적 락(Pessimistic Lock)
“충돌이 자주 발생할 것”이라고 가정하고, 데이터를 읽을 때부터 락을 건다.
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 락 획득
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
- 장점: 데이터 일관성 보장이 강력함
- 단점: 동시성 저하, 데드락 위험 존재
- 적합한 상황: 충돌 빈도가 높거나, 데이터 정합성이 매우 중요한 경우 (금융 거래 등)
낙관적 락(Optimistic Lock)
“충돌이 드물게 발생할 것”이라고 가정하고, DB 락 없이 버전(Version) 컬럼으로 충돌을 감지한다.
-- 조회 시 version 컬럼 함께 읽기
SELECT id, balance, version FROM accounts WHERE id = 1;
-- version = 5 라고 가정
-- 업데이트 시 version 조건 추가
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
-- 영향받은 행이 0이면 다른 트랜잭션이 먼저 수정한 것 → 재시도
- 장점: DB 락 없이 동시성이 높음
- 단점: 충돌 발생 시 재시도 로직이 필요함
- 적합한 상황: 충돌 빈도가 낮고 읽기 비중이 높은 경우
JPA에서는 @Version 어노테이션으로 낙관적 락을 손쉽게 사용할 수 있다:
@Entity
public class Account {
@Id
private Long id;
private int balance;
@Version
private int version; // JPA가 자동으로 버전 비교 및 증가 처리
}
데드락(Deadlock)
두 트랜잭션이 서로 상대방이 가진 락을 기다리는 상황이다.
트랜잭션 A: accounts(id=1) 락 보유 → accounts(id=2) 락 대기
트랜잭션 B: accounts(id=2) 락 보유 → accounts(id=1) 락 대기
두 트랜잭션 모두 영원히 대기 상태가 된다.
DB의 데드락 처리 방식
대부분의 RDBMS는 백그라운드에서 데드락 감지(Deadlock Detection) 를 수행한다. 데드락이 감지되면 한쪽 트랜잭션을 희생자(victim)로 선택해 롤백하고 에러를 반환한다.
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
데드락 예방 전략
- 항상 동일한 순서로 자원에 접근: 모든 트랜잭션이
id=1→id=2순으로 락을 획득하면 순환 대기가 발생하지 않는다. - 트랜잭션을 짧게 유지: 락을 보유하는 시간을 줄인다.
- 인덱스를 올바르게 사용: 인덱스 없이 UPDATE하면 테이블 전체에 락이 걸려 충돌 확률이 높아진다.
- 낙관적 락 도입 검토: 충돌이 드문 경우라면 DB 락 자체를 피하는 것이 근본적인 해결책이다.
정리
| 구분 | 공유 락 | 배타 락 |
|---|---|---|
| 용도 | 읽기 | 쓰기 |
| 다른 공유 락과 호환 | O | X |
| 다른 배타 락과 호환 | X | X |
- 락은 범위(테이블/로우)와 종류(공유/배타)로 분류된다.
- 비관적 락은 DB 수준 잠금, 낙관적 락은 버전 컬럼으로 충돌을 감지한다.
- 데드락은 DB가 자동 감지하고 롤백하지만, 예방 전략이 더 중요하다.
- 인덱스가 없는 WHERE 조건으로 UPDATE하면 테이블 전체에 락이 걸릴 수 있으므로 주의가 필요하다.