[Daily morning study] 데이터베이스 락(Lock)의 종류와 동작 방식

#daily morning study

Image


락(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

데드락 예방 전략

  1. 항상 동일한 순서로 자원에 접근: 모든 트랜잭션이 id=1id=2 순으로 락을 획득하면 순환 대기가 발생하지 않는다.
  2. 트랜잭션을 짧게 유지: 락을 보유하는 시간을 줄인다.
  3. 인덱스를 올바르게 사용: 인덱스 없이 UPDATE하면 테이블 전체에 락이 걸려 충돌 확률이 높아진다.
  4. 낙관적 락 도입 검토: 충돌이 드문 경우라면 DB 락 자체를 피하는 것이 근본적인 해결책이다.

정리

구분공유 락배타 락
용도읽기쓰기
다른 공유 락과 호환OX
다른 배타 락과 호환XX
  • 락은 범위(테이블/로우)와 종류(공유/배타)로 분류된다.
  • 비관적 락은 DB 수준 잠금, 낙관적 락은 버전 컬럼으로 충돌을 감지한다.
  • 데드락은 DB가 자동 감지하고 롤백하지만, 예방 전략이 더 중요하다.
  • 인덱스가 없는 WHERE 조건으로 UPDATE하면 테이블 전체에 락이 걸릴 수 있으므로 주의가 필요하다.