락과 데드락 (공유, 배타, 비관)
같은 데이터를 여러 트랜잭션이 동시에 만지면 결과가 어긋날 수 있다는 문제는 격리 수준만으로 다 풀리지 않는다. 격리 수준이 "어떤 이상 현상을 허용할지"를 정하는 정책이라면, 락(Lock)은 그 정책을 실제로 강제하는 메커니즘이다. 본 글은 락의 두 기본 유형인 공유락·배타락의 호환 규칙, 데드락이 발생하는 4가지 조건과 데이터베이스가 그것을 감지·해결하는 방식, 그리고 비관적 락과 낙관적 락의 선택 기준을 4학년 데이터베이스 시스템 수업과 본인의 캡스톤 운영 경험에 비추어 정리한다(출처: 위키백과 — Lock (database)). 제가 학교 캡스톤에서 결제와 적립금 동시 처리 로직을 짜다가 동시에 두 사용자가 같은 적립금 행을 건드리는 순간 한쪽이 무한 대기에 빠진 모습을 본 후로는 "락은 같이 쓸 수 있는 경우와 절대 같이 쓸 수 없는 경우 두 줄을 외워 두는 게 시작"이라는 사실을 손끝으로 받아들였다.

공유락(Shared Lock)과 배타락(Exclusive Lock)의 호환 규칙
데이터베이스에서 락은 기본적으로 두 종류만 있다. 공유락(Shared Lock, S락)은 읽기에 걸리는 락이고, 배타락(Exclusive Lock, X락)은 쓰기에 걸리는 락이다. 여기서 "걸린다"는 표현은 트랜잭션이 특정 행이나 테이블에 대해 다른 트랜잭션의 행동을 제한하는 표식을 남긴다는 의미다. 둘의 호환 규칙은 한 줄로 외워진다.
S락 요청 X락 요청
S락 보유 OK 대기
X락 보유 대기 대기
즉 같은 행에 S락이 여러 개 동시에 걸리는 것은 허용되지만(여러 명이 동시에 읽기), 누군가가 X락을 잡고 있는 동안에는 다른 누구도 S락·X락 모두 잡을 수 없다(혼자만 쓰기). 데이터베이스가 ACID의 격리성(Isolation)을 보장하는 가장 기초적 메커니즘이 바로 이 호환 규칙이다.
가장 단순한 형태의 명시적 락 SQL은 다음과 같다.
-- 공유락: 다른 읽기는 허용, 쓰기는 차단
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
-- 배타락: 다른 모든 접근 차단
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
FOR UPDATE는 실무에서 가장 자주 만나는 키워드이며, "이 행을 지금 잠깐 잡아 두고, 내가 UPDATE를 마칠 때까지 다른 누구도 못 만지게 해 줘"라는 의미다. 잔액 차감·재고 차감·티켓 예매처럼 "읽고 → 판단하고 → 쓴다"는 흐름 전체가 한 단위로 보호되어야 하는 영역에서 표준 패턴이다.
다만 락은 모든 문제를 푸는 도구가 아니라 만들어내는 도구이기도 하다. 솔직히 제 경험상 입문 단계에서는 "이상 현상 막으려면 다 FOR UPDATE 박으면 되겠다"는 식으로 접근하기 쉬운데, 락 범위를 넓히면 처리량이 직선으로 떨어지고 그 끝에서 만나는 게 다음 절의 주인공인 데드락이다.
데드락(Deadlock)의 4가지 조건과 감지·해결
데드락이란 둘 이상의 트랜잭션이 서로가 가진 락이 풀리기를 기다리며 영원히 대기하는 상태를 가리킨다. 운영체제의 데드락과 정확히 같은 문제이며, 동일하게 4가지 조건이 모두 충족되어야 발생한다(출처: 위키백과 — Deadlock).
첫째, 상호 배제(Mutual Exclusion)다. 자원이 한 번에 한 트랜잭션만 쓸 수 있어야 한다. X락이 정확히 이 조건을 만든다. 둘째, 점유와 대기(Hold and Wait)다. 트랜잭션이 자원 하나를 잡은 채 다음 자원을 기다린다. 셋째, 비선점(No Preemption)이다. 다른 트랜잭션이 강제로 락을 뺏을 수 없다. 넷째, 순환 대기(Circular Wait)다. 트랜잭션들이 서로를 빙 둘러 가며 기다린다. 네 조건 중 하나라도 깨지면 데드락은 원천적으로 발생하지 않는다.
실제로 데드락이 만들어지는 가장 흔한 패턴은 다음과 같다.
-- 트랜잭션 A
UPDATE accounts SET balance = balance - 1000 WHERE id = 1; -- 행 1 X락
UPDATE accounts SET balance = balance + 1000 WHERE id = 2; -- 행 2 X락 대기
-- 트랜잭션 B (동시 실행)
UPDATE accounts SET balance = balance - 500 WHERE id = 2; -- 행 2 X락
UPDATE accounts SET balance = balance + 500 WHERE id = 1; -- 행 1 X락 대기
A는 행 1을 잡은 채 행 2를 기다리고, B는 행 2를 잡은 채 행 1을 기다린다. 정확히 순환 대기가 만들어졌다. 본인이 학교 캡스톤에서 마주친 적립금 데드락도 정확히 이 형태였고, 두 사용자의 적립금 이체 요청이 거의 같은 시각에 들어오면서 한쪽이 영원히 끝나지 않았다.
대부분의 데이터베이스는 이런 상황을 자동으로 감지한다. MySQL InnoDB·PostgreSQL은 락 대기 그래프(Wait-for Graph)에 순환이 생기는 순간 한쪽 트랜잭션을 강제 종료(rollback)시키고 다른 한쪽을 통과시킨다. 종료된 쪽은 애플리케이션에서 재시도하면 끝난다. 즉 데드락은 발생을 막는 것보다 발생을 빠르게 처리하는 것이 운영 표준이며, 입문자가 자주 오해하는 지점이 바로 여기다. 데드락은 "절대 일어나면 안 되는 사고"가 아니라 "일어나도 자동 복구되는 정상 흐름"의 일부로 다뤄야 한다.
흔한 오해 하나를 짚으면, "락 타임아웃을 짧게 잡으면 데드락이 안 생긴다"는 통념이 있다. 그러나 타임아웃은 데드락이 아니라 락 대기 자체를 끊는 장치다. 데드락 감지는 운영체제·DBMS 차원의 별도 메커니즘이며, 타임아웃과 무관하게 동작한다.
비관락(Pessimistic Lock)과 낙관락(Optimistic Lock) 선택 기준
마지막으로 락을 거는 시점에 대한 두 가지 철학이 있다. 비관락은 "충돌이 자주 일어날 것이다"라고 가정하고 처음부터 락을 강하게 거는 방식이며, 위에서 본 SELECT ... FOR UPDATE가 대표적이다. 반면 낙관락은 "충돌은 드물게만 일어날 것이다"라고 가정하고, 락을 전혀 걸지 않다가 마지막 UPDATE 시점에 데이터가 그동안 바뀌지 않았는지 확인하는 방식이다(출처: 위키백과 — Optimistic concurrency control).
낙관락의 표준 구현은 버전 컬럼(version)을 두는 것이다. UPDATE 시 WHERE 절에 "지금 알고 있는 버전이 맞을 때만 갱신"이라는 조건을 박는다.
-- 1) 읽을 때
SELECT id, balance, version FROM accounts WHERE id = 1;
-- (이때 version = 7로 받았다고 가정)
-- 2) 쓸 때 — version이 그대로 7이어야 성공
UPDATE accounts SET balance = balance - 1000, version = version + 1
WHERE id = 1 AND version = 7;
다른 트랜잭션이 그사이 같은 행을 갱신했다면 version이 8로 올라가 있어서 위 UPDATE는 0행을 업데이트하고 끝난다. 애플리케이션은 그 결과를 보고 충돌을 감지해 재시도하거나 사용자에게 알린다.
선택 기준은 한 줄로 정리되는데, 충돌 빈도가 높으면 비관락, 낮으면 낙관락이다. 같은 좌석을 수십 명이 동시에 노리는 좌석 예매·인기 상품 결제 같은 영역은 비관락이 안전하다. 반면 사용자 본인의 프로필 수정·블로그 글 편집처럼 충돌이 거의 일어나지 않는 영역은 낙관락이 처리량 측면에서 압도적으로 유리하다. 솔직히 이건 시험엔 자주 안 나오지만 실무에서 가장 자주 의사결정이 필요한 지점이다.
락은 데이터베이스의 신뢰성을 만드는 도구이면서 동시에 처리량을 무너뜨릴 수 있는 도구다. 공유락·배타락의 호환 규칙으로 시작해, 데드락의 4가지 조건과 자동 감지 흐름을 이해하고, 비관 vs 낙관의 선택 기준을 손에 잡으면, 격리 수준 위에 얹히는 동시성 제어의 두 번째 층이 완성된다.