- Lock의 개념과 필요성을 이해한다.
- 낙관적 락과 비관적 락의 차이를 이해하고, 상황에 따라 선택할 수 있다.
여러 세션이 동시에 같은 데이터를 건드릴 때, 꼬이지 않도록 잠시 잠가두는 안전 장치
트랜잭션 챕터에서 보았듯, 세션 A가 커밋을 수행하지 않은 상태에서 세션 B가 동시에 같은 데이터를 수정하면 원자성이 위반될 수 있다.
이를 방지하려면, 한 세션이 데이터를 수정하는 동안(커밋 또는 롤백 전까지) 다른 세션이 해당 데이터에 접근하지 못하도록 막아야 한다.
이것이 락(Lock) 이다.
데이터베이스는 다중 사용자가 접근하는 시스템이기 때문에, 한 사용자가 데이터를 읽거나 수정하는 동안 다른 사용자의 작업을 잠시 막는 방법이 필요하다.
다음 상황을 가정해보자.
서여와 봉구스는 같은 공동 계좌 (잔액 100,000원)에서 동시에 50,000원씩 인출하려 한다.
| 시점 | 서여 (세션 A) | 봉구스 (세션 B) | 실제 잔액 |
|---|---|---|---|
| T1 | 잔액 조회 → 100,000원 | 100,000원 | |
| T2 | 잔액 조회 → 100,000원 | 100,000원 | |
| T3 | 50,000원 차감 후 저장 → 50,000원 | 50,000원 | |
| T4 | 50,000원 차감 후 저장 → 50,000원 | 50,000원 ← 잘못됨 |
봉구스는 T2에 읽은 100,000원을 기준으로 계산하기 때문에, T3에서 서여가 이미 차감한 사실을 모른다.
결과적으로 서여의 인출이 덮어씌워진다. 이를 갱신 손실(Lost Update) 이라 한다.
락 없음 → 둘 다 잔액 100,000원 조회 → 각자 50,000원 빼서 저장 → 최종 잔액 50,000원 (서여의 인출이 덮어씌워짐, Lost Update)
락 있음 → 서여 먼저 처리 후 봉구스 처리 → 최종 잔액 0원
락은 트랜잭션 단위로 동작한다.
- 트랜잭션 시작
- 데이터에 접근할 때 락 획득
- 데이터 읽기 / 수정
- 커밋 또는 롤백
- 락 해제 → 다른 세션이 해당 데이터에 접근 가능
락은 트랜잭션이 시작되면서 걸리고, 커밋 또는 롤백 될 때 풀린다.
그 사이 시간 동안, 다른 세션은 그 데이터에 접근하지 못하고 줄 서서 기다려야 한다.
락을 보유하고 있는 시간이 길수록, 다른 세션의 대기 시간도 늘어난다.
따라서 트랜잭션은 짧게 유지하는 것이 중요하다.
락은 언제, 얼마나 적극적으로 잠글 것인가 에 따라 두 가지 전략으로 나뉜다.
혹시 모르니, 내가 끝날 때까지 아무도 건들지 마!
- 충돌이 자주 발생할 것이라고 가정하고, 데이터를 읽는 순간부터 미리 락을 건다. (선점 락)
- 무조건 잠그고 작업한다.
- 데이터를 읽을 때 즉시 락 획득 (
SELECT FOR UPDATE) - 다른 세션이 해당 데이터를 수정하거나 FOR UPDATE로 읽으려 하면 대기 상태로 전환 (일반 SELECT는 가능)
- 락을 보유한 세션이 커밋 또는 롤백하면 락 해제
- 대기 중이던 세션이 락을 획득하고 작업 진행
-- 비관적 락 예시 (PostgreSQL / MySQL 공통)
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 이 시점부터 다른 세션은 id = 1 행을 수정하거나 FOR UPDATE로 읽을 수 없음
UPDATE accounts SET balance = balance - 50000 WHERE id = 1;
COMMIT;도서관에서 내가 책을 빌리면, 사서가 해당 책에 "대출 중 - 반납 전까지 접근 불가" 딱지를 붙인다.
다른 사람은 내가 반납할 때까지 그 책을 빌릴 수 없다.
충돌이 나면 절대 안 되는 핵심 데이터
- 은행 계좌 잔액 변경
- 항공권 / 콘서트 티켓 좌석 예매
- 창고 재고 처리
| 내용 | |
|---|---|
| 장점 | 충돌이 원천 차단된다. 데이터 정합성이 보장된다. |
| 단점 | 락이 오래 유지되면 다른 세션이 대기해야 한다. 트래픽이 몰리면 성능 저하로 이어질 수 있다. |
어차피 충돌 잘 안 나겠지. 근데 마지막에 확인은 해볼게.
- 충돌이 드물 것이라고 가정하고, 데이터를 읽을 때 락을 걸지 않는다. (비선점 락)
- 대신 커밋 시점에 '내가 읽었을 때와 지금 데이터가 같은가?'를 확인한다.
- 읽을 때 → 잠그지 않음
- 수정 후 커밋할 때 → '내가 읽었을 때랑 데이터가 바뀌었나?' 확인
- 바뀌었으면 → 잠그는 게 아니라 그냥 에러를 던지고 처음부터 다시 시도
테이블에 version 컬럼을 추가해 변경 여부를 추적한다.
버전 번호를 체크할때는 날짜, 타임 스탬프, 체크섬, 해시등의 방법을 사용한다.
- 데이터를 읽을 때
version값도 함께 읽어둔다. - 수정 후 커밋할 때, DB에 저장된
version이 읽었을 때와 같은지 확인한다. - 같다면 → 커밋 성공,
version1 증가 - 다르다면 → 누군가 먼저 수정했다는 뜻 → 충돌(에러) 발생 → 재시도 필요
- 레코드의 버전이 다르면, 트랜잭션을 중단하고 다시 시작할 수 있다.
-- 낙관적 락 예시
-- 1. 데이터 읽기 (락 없음)
SELECT likes, version FROM posts WHERE id = 42;
-- 결과: likes = 100, version = 5
-- 2. 커밋 시 version 확인
UPDATE posts
SET likes = 101, version = 6
-- 내가 읽었을 때의 version과 지금 DB version이 같아야만 업데이트 됨
WHERE id = 42 AND version = 5;
-- 업데이트된 행이 0개라면 → 다른 세션이 먼저 수정한 것 → 충돌 처리책을 빌릴 때 자물쇠 없이 그냥 빌려간다.
반납할 때 '이 책, 내가 빌려간 이후로 다른 사람이 수정했나요?' 확인한다.
수정됐다면 다시 최신 내용을 읽고 처음부터 다시 시도해야 한다.
동시 접근은 많지만, 같은 데이터를 동시에 수정하는 경우가 드물 때
- 게시글 좋아요 수, 상품 조회수
- 포인트 적립
- 댓글 작성
| 내용 | |
|---|---|
| 장점 | 락을 거의 걸지 않아서 성능이 좋다. 대기 없이 병렬 처리가 가능하다. 데드락(Deadlock) 발생 가능성이 낮다. |
| 단점 | 충돌이 발생했을 때 데이터를 다시 읽고, 업데이트해야 하는 추가 작업이 필요하다. (충돌을 해결하기 위해선 충돌 발생 시점부터 트랜잭션을 다시 시작해야 하기 때문) 충돌이 잦은 환경에서는 오히려 성능이 나빠진다. |
| 비관적 락 | 낙관적 락 | |
|---|---|---|
| 언제 잠금? | 읽는 순간 | 커밋 시점 |
| 충돌 처리 | 대기 (자동 처리) | 에러 → 재시도 (직접 구현) |
| 적합한 상황 | 충돌 빈도 높음, 정합성 최우선 | 충돌 빈도 낮음, 성능 중요 |
| 성능 | 트래픽 많을수록 저하 가능 | 충돌 없을 때 빠름 |
비관적 락은 낙관적 락보다 데이터 정합성이 우수하다.
하지만 락을 선점한 채 서로 다른 데이터를 기다리는 상황이 생기면, 두 세션이 영원히 진행되지 못하는 교착 상태(Deadlock) 가 발생할 수 있다.