공유 락
여러 트랜잭션이 동시에 읽기 작업을 수행할 수 있도록 허용하되, 쓰기 작업은 차단하는 락입니다.
왜 필요한가
[ 락이 없으면 ]
트랜잭션 A: 데이터 읽는 중 (잔액: 1000원)
트랜잭션 B: 데이터 수정 (잔액: 1000원 → 500원)
트랜잭션 A: 읽기 완료 (잔액: 1000원... 인 줄 알았는데 이미 500원)
→ 데이터 불일치 (Dirty Read)
[ 공유 락 사용 ]
트랜잭션 A: 공유 락 획득 → 데이터 읽기 중
트랜잭션 B: 수정하려고 배타 락 요청 → 대기
트랜잭션 A: 읽기 완료 → 공유 락 해제
트랜잭션 B: 이제 배타 락 획득 → 수정
→ 데이터 일관성 보장
공유 락 vs [[배타 락
┌────────────────────────────────────────────────────────┐
│ 공유 락 (Shared Lock, S Lock) │
│ = Read Lock │
│ │
│ "나 읽는 중이야, 너희도 읽어도 돼. 근데 수정은 안 돼" │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ 배타 락 (Exclusive Lock, X Lock) │
│ = Write Lock │
│ │
│ "나 수정 중이야, 아무도 읽거나 쓰면 안 돼" │
└────────────────────────────────────────────────────────┘
| 항목 | 공유 락 (S) | 배타 락 (X) |
| 용도 | 읽기 (SELECT) | 쓰기 (INSERT, UPDATE, DELETE) |
| 동시 공유 락 | O 허용 | X 불허 |
| 동시 배타 락 | X 불허 | X 불허 |
| 다른 이름 | Read Lock | Write Lock |
락 호환성 매트릭스
요청하는 락
S X
┌──────┬──────┐
보유 중 S │ O │ X │
인 락 X │ X │ X │
└──────┴──────┘
O = 호환 (동시 보유 가능)
X = 충돌 (대기해야 함)
공유 락 + 공유 락 = O 동시 가능 (여러 트랜잭션이 같이 읽기)
공유 락 + 배타 락 = X 대기 (읽는 중에 쓰기 불가)
배타 락 + 공유 락 = X 대기 (쓰는 중에 읽기 불가)
배타 락 + 배타 락 = X 대기 (쓰는 중에 쓰기 불가)
동작 시나리오
시나리오 1: 읽기 + 읽기 = 동시 가능
시간 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→
트랜잭션 A: [S락 획득]───읽기───[S락 해제]
트랜잭션 B: [S락 획득]────읽기────[S락 해제]
트랜잭션 C: [S락 획득]──읽기──[S락 해제]
↑
동시에 공유 락 보유 가능
시나리오 2: 읽기 + 쓰기 = 대기
시간 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→
트랜잭션 A: [S락 획득]───읽기───[S락 해제]
트랜잭션 B: [X락 요청]──대기──대기──[X락 획득]───쓰기───[X락 해제]
↑ ↑
S락이 있어서 대기 S락 해제되면 X락 획득
시나리오 3: 쓰기 + 읽기 = 대기
시간 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→
트랜잭션 A: [X락 획득]───쓰기───[X락 해제]
트랜잭션 B: [S락 요청]──대기──대기──[S락 획득]───읽기───
↑ ↑
X락이 있어서 대기 X락 해제되면 S락 획득
SQL에서 락 사용
MySQL / InnoDB
-- 공유 락 (읽기 락)
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- 또는 (구버전 문법)
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
-- 배타 락 (쓰기 락)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 일반 SELECT는 락을 걸지 않음 (MVCC로 처리)
SELECT * FROM accounts WHERE id = 1;
PostgreSQL
-- 공유 락
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- 배타 락
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 락 대기 없이 즉시 실패
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- 락 걸린 행 건너뛰기
SELECT * FROM accounts WHERE id = 1 FOR UPDATE SKIP LOCKED;
실제 사용 예시
계좌 잔액 조회 — 읽기 중 변경 방지
-- 트랜잭션 A: 잔액 확인 (읽는 동안 변경되면 안 됨)
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR SHARE;
-- balance = 1000
-- 이 시점에 다른 트랜잭션이 UPDATE하면 대기
-- 업무 처리 (예: 리포트 생성)
-- ...
COMMIT; -- 공유 락 해제
-- 트랜잭션 B: 잔액 수정 시도
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- ↑ 트랜잭션 A가 공유 락을 보유 중이면 대기
-- 트랜잭션 A가 COMMIT/ROLLBACK하면 실행됨
COMMIT;
재고 확인 — 여러 사용자가 동시에 조회
-- 사용자 A, B, C 모두 동시에 재고 조회 가능
SELECT stock FROM products WHERE id = 100 FOR SHARE;
-- 공유 락은 서로 호환되므로 동시 실행 O
-- 재고 수정은 모든 공유 락이 해제될 때까지 대기
UPDATE products SET stock = stock - 1 WHERE id = 100;
락의 범위 (Lock Granularity)
┌─── 데이터베이스 락 ──────────────────────────────┐
│ 전체 DB에 락 → 동시성 최저 │
├─── 테이블 락 ───────────────────────────────────┤
│ 특정 테이블 전체에 락 │
├─── 페이지 락 ───────────────────────────────────┤
│ 디스크 페이지 단위 락 │
├─── 행 락 (Row Lock) ────────────────────────────┤
│ 특정 행에만 락 → 동시성 최고 (가장 많이 사용) │
└─────────────────────────────────────────────────┘
-- 행 수준 공유 락 (특정 행만)
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- id = 1인 행만 락 → 다른 행은 자유롭게 접근 가능
-- 테이블 수준 락 (MySQL)
LOCK TABLES accounts READ; -- 테이블 전체 공유 락
LOCK TABLES accounts WRITE; -- 테이블 전체 배타 락
UNLOCK TABLES;
데드락 (Deadlock) 주의
트랜잭션 A 트랜잭션 B
─────────── ───────────
행 1에 S락 획득 행 2에 S락 획득
│ │
▼ ▼
행 2에 X락 요청 → 대기 행 1에 X락 요청 → 대기
│ ↖ ↗ │
└───────── 서로 대기 ─────────┘
(DEADLOCK!)
-- 데드락 발생 예시
-- 트랜잭션 A
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- ... 다른 작업 ...
UPDATE accounts SET balance = 500 WHERE id = 2; -- 대기
-- 트랜잭션 B
BEGIN;
SELECT * FROM accounts WHERE id = 2 FOR SHARE;
-- ... 다른 작업 ...
UPDATE accounts SET balance = 300 WHERE id = 1; -- 대기 → DEADLOCK
해결:
DBMS가 데드락을 감지하면 한쪽 트랜잭션을 강제 롤백
→ "Deadlock found when trying to get lock" 에러 발생
→ 애플리케이션에서 재시도 로직 필요
| 격리 수준 | 공유 락 동작 |
| READ UNCOMMITTED | 락 없이 읽기 (Dirty Read 허용) |
| READ COMMITTED | 읽기 완료 즉시 공유 락 해제 |
| REPEATABLE READ | 트랜잭션 끝까지 공유 락 유지 |
| SERIALIZABLE | 범위까지 공유 락 (가장 엄격) |
-- REPEATABLE READ에서
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- 트랜잭션이 끝날 때까지 S락 유지
-- 다른 트랜잭션이 이 행을 수정 불가
COMMIT; -- 여기서 S락 해제
낙관적 락 vs 비관적 락
[ 비관적 락 (Pessimistic Lock) ]
"충돌이 날 것 같아, 미리 락을 걸자"
→ SELECT ... FOR SHARE / FOR UPDATE
→ 공유 락, 배타 락이 비관적 락의 구현
[ 낙관적 락 (Optimistic Lock) ]
"충돌이 별로 없을 거야, 일단 작업하고 나중에 확인하자"
→ 버전 번호나 타임스탬프로 충돌 감지
→ 락을 실제로 걸지 않음
-- 낙관적 락 예시 (버전 기반)
SELECT id, balance, version FROM accounts WHERE id = 1;
-- balance = 1000, version = 5
UPDATE accounts
SET balance = 500, version = version + 1
WHERE id = 1 AND version = 5;
-- version이 5가 아니면 0 rows affected → 충돌 감지
| 항목 | 비관적 락 (공유/배타) | 낙관적 락 |
| 충돌 예상 | 높음 | 낮음 |
| 락 오버헤드 | 있음 | 없음 |
| 충돌 시 | 대기 | 재시도 |
| 적합한 상황 | 쓰기 많음, 충돌 빈번 | 읽기 많음, 충돌 드묾 |
핵심 정리
공유 락은 여러 트랜잭션이 동시에 읽기를 허용하되, 쓰기는 차단하는 락입니다. 공유 락끼리는 호환되어 동시 보유가 가능하지만, 배타 락과는 충돌하여 대기합니다. SQL에서는 SELECT ... FOR SHARE로 명시적으로 공유 락을 걸 수 있으며, 읽기 작업 중 데이터 변경을 방지하여 데이터 일관성을 보장합니다. 다만 여러 트랜잭션이 공유 락을 보유한 상태에서 배타 락을 요청하면 데드락이 발생할 수 있으므로, 락 순서를 일관되게 유지하거나 타임아웃을 설정하는 등의 주의가 필요합니다.