데이터베이스 락 이해하기
목차
- 목적
- 왜 락이 필요한가
- 락 기본 종류
- 락 범위
- 격리수준과 락 관계
- 비관적 락 vs 낙관적 락
- 데드락 실무 대응
- Spring/JPA 적용 포인트
- 선택 가이드
- 빠른 선택 표
- 체크리스트
md_doc테이블 기준 실전 예제
1. 목적
- 동시성 환경에서 데이터 정합성을 지키기 위한 락의 핵심 개념 정리
- 트랜잭션 격리수준, 비관적/낙관적 락, 데드락 대응까지 실무 관점으로 연결
- Spring/JPA 기준으로 바로 적용 가능한 선택 기준 제공
2. 왜 락이 필요한가
동시 수정 구간의 대표 문제:
- Lost Update: 마지막 저장이 이전 변경을 덮어씀
- Dirty Read: 커밋되지 않은 값을 읽음
- Non-Repeatable Read: 같은 조회인데 값이 바뀜
- Phantom Read: 같은 조건 재조회 시 행 수가 바뀜
md_doc 기준 문제 예시:
| 항목 | 락/격리 제어 부족 상황 | 실제 데이터 문제 |
|---|---|---|
| Lost Update | A, B가 md_doc_id=10을 조회 후 각각 title 수정 저장 | 마지막 커밋이 먼저 커밋한 값 덮어씀 (A 제목 소실) |
| Dirty Read | A가 status=1 -> 9로 변경 후 미커밋, B가 그 값 조회 | A 롤백 시 B가 존재하지 않는 상태값 기반 로직 실행 |
| Non-Repeatable Read | A 트랜잭션에서 md_doc_id=11을 두 번 조회, 사이에 B가 content 수정 커밋 | 같은 트랜잭션 내 1차/2차 조회 결과 불일치 |
| Phantom Read | A가 status=1 AND sort_order BETWEEN 100 AND 200 목록 조회, 사이에 B가 같은 구간 INSERT 커밋 | A의 재조회 행 수 증가, 배치/페이지네이션 기준 흔들림 |
- 락: 동시성 충돌 제어 핵심 도구
- 격리수준: 락/스냅샷 정책 강도 결정 요소
3. 락 기본 종류
3.1 공유 락(Shared, S-Lock)
- 읽기 잠금
- 다른 트랜잭션의 읽기는 허용
- 쓰기(배타 락)는 차단
3.2 배타 락(Exclusive, X-Lock)
- 쓰기 잠금
- 다른 트랜잭션의 읽기/쓰기(또는 쓰기) 충돌을 차단
- 일반적으로
UPDATE,DELETE, 일부SELECT ... FOR UPDATE에서 사용
4. 락 범위(Granularity)
- Row Lock: 특정 행만 잠금 (가장 일반적, 동시성에 유리)
- Table Lock: 테이블 전체 잠금 (동시성 낮지만 단순)
- Page Lock: 일부 엔진에서 페이지 단위 잠금
- Key-Range/Gap Lock: 인덱스 구간 잠금 (팬텀 리드 방지 목적, MySQL InnoDB에서 중요)
핵심: 락 범위 확대 = 안전성 상승, 처리량 하락
5. 격리수준과 락 관계
READ UNCOMMITTED: 정합성 위험 큼, 실무에서 거의 사용하지 않음READ COMMITTED: 커밋된 데이터만 읽음 (PostgreSQL 기본)REPEATABLE READ: 트랜잭션 중 같은 조회 결과를 더 안정적으로 보장 (MySQL InnoDB 기본)SERIALIZABLE: 가장 강한 격리, 동시성 비용 큼
주의:
- 높은 격리수준 = 무조건 락 증가, 단순화 금지
- DB 엔진 동작: MVCC(스냅샷) + 락 혼합
6. 비관적 락 vs 낙관적 락
6.1 비관적 락(Pessimistic Lock)
전제: 충돌 가능성 높음, 선잠금 전략
예시:
SELECT * FROM account WHERE id = 1 FOR UPDATE;
특징:
- 장점: 충돌 시점이 빠르고 데이터 충돌 방지가 명확함
- 단점: 대기/타임아웃/데드락 가능성 증가, 처리량 저하 가능
적합한 경우:
- 재고 차감, 계좌 이체처럼 충돌 비용이 큰 쓰기 중심 구간
6.2 낙관적 락(Optimistic Lock)
전제: 충돌 가능성 낮음, 커밋 시 버전 비교로 충돌 검출
JPA 예시:
@Entity
public class Product {
@Id
private Long id;
@Version
private Long version;
}
특징:
- 장점: 락 대기가 거의 없어 읽기/분산 환경에 유리
- 단점: 충돌 시 재시도 로직 필요
적합한 경우:
- 조회가 많고 동시에 같은 행을 갱신하는 비율이 낮은 서비스
7. 데드락(Deadlock) 실무 대응
데드락: 서로의 락 해제를 기다리는 순환 대기 상태 운영 포인트: 완전 제거보다 빠른 감지/복구 전략
예방 원칙:
- 항상 동일한 순서로 자원 잠그기 (예: 작은 ID -> 큰 ID)
- 트랜잭션을 짧게 유지
- 불필요한
SELECT ... FOR UPDATE최소화 - 인덱스 미비로 인한 광범위 락 방지
복구 원칙:
- DB 데드락 예외를 잡아 재시도(backoff 포함)
- 재시도 횟수 제한 및 실패 로깅/알람
8. Spring/JPA 적용 포인트
비관적 락 예시:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.id = :id")
Optional<Product> findForUpdate(@Param("id") Long id);
낙관적 락 예시:
- 엔티티에
@Version필드 추가 OptimisticLockException발생 시 재시도 정책 적용
트랜잭션 설정 팁:
- 락 획득이 필요한 메서드는
@Transactional경계를 명확히 관리 - 외부 API 호출/파일 I/O를 트랜잭션 내부에 오래 두지 않기
- 긴 트랜잭션은 락 유지 시간 증가로 장애 전파 가능
9. 선택 가이드(요약)
- 충돌 빈도 높음 + 정합성 절대 우선: 비관적 락 우선 검토
- 충돌 빈도 낮음 + 처리량 우선: 낙관적 락 + 재시도
- 팬텀/범위 정합성 중요: 인덱스 + 격리수준 + 구간 락 영향 함께 점검
- 장애 대응: 타임아웃/데드락/재시도 정책을 코드와 운영 알람에 같이 반영
10. 빠른 선택 표
| 상황 | 권장 접근 | 이유 | 주의점 |
|---|---|---|---|
| 같은 문서를 동시에 자주 수정 | 비관적 락 (FOR UPDATE) | 충돌을 초기에 직렬화 | 락 대기/타임아웃 증가 |
| 조회는 많고 동시 수정은 드묾 | 낙관적 락 (@Version) | 평소 락 대기 최소화 | 충돌 시 재시도 로직 필수 |
| 정렬 구간 재배치 작업 | 작은 배치 + 짧은 트랜잭션 | 락 점유 시간 축소 | 범위 잠금으로 INSERT 막힘 가능 |
| 다건 갱신/삭제 | PK 순서 고정 처리 | 데드락 확률 감소 | 순서 불일치 시 데드락 증가 |
11. 체크리스트
- 핵심 갱신 쿼리에 적절한 인덱스가 있는가
- 락이 필요한 구간과 불필요한 구간을 분리했는가
- 트랜잭션 길이가 짧은가 (외부 연동 포함 여부 점검)
- 데드락/락 타임아웃 예외 처리와 재시도 정책이 있는가
- 운영 로그에서 락 대기 시간이 관측 가능한가
12. md_doc 테이블 기준 실전 예제
테이블 역할(문맥):
- 문서/페이지 본문을 저장하는 콘텐츠 테이블
- 운영 화면에서 목록 조회(
status,sort_order)와 단건 편집(md_doc_id)이 자주 발생 - 즉, "목록 정렬 작업"과 "동일 문서 동시 편집"이 락 충돌 포인트
대상 DDL 핵심:
- PK:
md_doc_id - 보조 인덱스:
idx_md_doc_status_sort_mddoc (status, sort_order, md_doc_id DESC) - 엔진: InnoDB
주요 컬럼 요약:
| 컬럼 | 용도 | 락 관점에서 중요한 점 |
|---|---|---|
md_doc_id (PK) | 문서 고유 ID | 단건 UPDATE/DELETE 충돌의 기준 키 |
status | 문서 상태값 | 목록 필터 조건에 자주 사용, 범위/다건 잠금에 영향 |
sort_order | 목록 정렬 순서 | 재정렬 배치 시 동시 갱신 충돌 빈도 높음 |
title, content | 문서 내용 | 편집 트랜잭션에서 실제 변경 대상 |
date_modified | 수정 시각 | UPDATE마다 같이 변경되어 row write 충돌에 포함 |
전제:
- 두 개의 세션(A/B)에서 같은 DB에 접속
- 기본 격리수준: MySQL InnoDB 기본
REPEATABLE READ
12.1 같은 행 업데이트 충돌 (lock wait timeout)
상황: 같은 문서(md_doc_id = 10)를 동시에 수정.
Session A:
START TRANSACTION;
UPDATE md_doc
SET title = 'A가 수정중'
WHERE md_doc_id = 10;
-- 아직 COMMIT 안 함 (X 락 유지)
Session B:
START TRANSACTION;
UPDATE md_doc
SET title = 'B가 수정시도'
WHERE md_doc_id = 10;
-- A가 먼저 잡은 X 락 때문에 대기
결과:
- A가 커밋하면 B가 이어서 실행
- A가 오래 잡고 있으면 B는
Lock wait timeout exceeded(에러 1205) 가능
핵심:
- PK 조건
UPDATE: 대상 행 배타 락(X 락) - 긴 트랜잭션: 충돌 확률 급증
12.2 SELECT ... FOR UPDATE가 만든 대기
상황: 편집 시작 시 문서를 선점하려고 조회 락 사용.
Session A:
START TRANSACTION;
SELECT md_doc_id, title
FROM md_doc
WHERE md_doc_id = 11
FOR UPDATE;
-- 11번 행에 쓰기 의도 락 획득
Session B:
START TRANSACTION;
UPDATE md_doc
SET content = '다른 사용자가 수정'
WHERE md_doc_id = 11;
-- A 트랜잭션 종료 전까지 대기
핵심:
FOR UPDATE: 조회 형태 + 강한 쓰기 의도 락- 편집 진입 구간 무분별 사용: 락 대기 누적
12.3 범위 잠금(Next-Key/Gap Lock)으로 인한 INSERT 대기
상황: 정렬 구간 조회를 FOR UPDATE로 잠금.
Session A:
START TRANSACTION;
SELECT md_doc_id, status, sort_order
FROM md_doc
WHERE status = 1
AND sort_order BETWEEN 100 AND 200
FOR UPDATE;
Session B:
START TRANSACTION;
INSERT INTO md_doc (title, content, status, sort_order, slug)
VALUES ('신규 문서', '...', 1, 150, 'new-doc');
-- A가 잡은 인덱스 구간 락에 걸려 대기 가능
핵심:
idx_md_doc_status_sort_mddoc인덱스를 타는 범위 조회 +FOR UPDATE는 특정 행뿐 아니라 "인덱스 구간"까지 잠금 가능- 해당 구간으로 들어오는
INSERT차단 가능 - 큐/정렬 로직의 대표 병목 패턴
12.4 서로 다른 행을 반대 순서로 잠가서 데드락
상황: 두 트랜잭션이 20번/21번 문서를 반대 순서로 수정.
Session A:
START TRANSACTION;
UPDATE md_doc SET title = 'A-1' WHERE md_doc_id = 20; -- 20 락 획득
-- 잠시 대기
UPDATE md_doc SET title = 'A-2' WHERE md_doc_id = 21; -- B가 먼저 잡았으면 대기
Session B:
START TRANSACTION;
UPDATE md_doc SET title = 'B-1' WHERE md_doc_id = 21; -- 21 락 획득
-- 잠시 대기
UPDATE md_doc SET title = 'B-2' WHERE md_doc_id = 20; -- A가 먼저 잡았으면 대기
결과:
- InnoDB가 데드락을 감지하고 둘 중 하나를 롤백 (
Deadlock found, 에러 1213)
핵심:
- 동일 자원 잠금 순서 통일(
작은 md_doc_id -> 큰 md_doc_id): 데드락 감소
12.5 운영에서 바로 적용할 쿼리 습관
- 단건 갱신은 가능하면 PK(
md_doc_id)로 정확히 집어 갱신 - 정렬 변경 배치 작업은 작은 단위로 나눠서 짧게 커밋
FOR UPDATE는 꼭 필요한 구간에서만 사용- 긴 트랜잭션 내부에서 외부 API 호출 금지
- 락 대기 진단 시
SHOW ENGINE INNODB STATUS\G로 최근 데드락 확인
12.6 시나리오 요약 표
| 시나리오 | Session A | Session B | 결과 | 대표 에러 |
|---|---|---|---|---|
같은 md_doc_id 동시 UPDATE | UPDATE ... WHERE md_doc_id=? 후 미커밋 | 같은 PK UPDATE | B 대기 후 A 커밋 시 진행 | 1205 가능 |
FOR UPDATE 선점 후 수정 | SELECT ... FOR UPDATE | 같은 행 UPDATE | B 대기 | 1205 가능 |
범위 FOR UPDATE + INSERT | status/sort_order 범위 잠금 | 해당 범위로 INSERT | INSERT 대기 | 1205 가능 |
| 반대 순서 다건 수정 | 20 -> 21 순서 UPDATE | 21 -> 20 순서 UPDATE | 데드락 감지, 한쪽 롤백 | 1213 |
12.7 에러코드 대응 표
| 에러 코드 | 의미 | 1차 대응 | 애플리케이션 전략 |
|---|---|---|---|
| 1205 | 락 대기 시간 초과 | 잠금 트랜잭션 길이 단축, 인덱스 점검 | 짧은 backoff 후 제한 재시도 |
| 1213 | 데드락으로 강제 롤백 | 잠금 순서 통일, 트랜잭션 축소 | 멱등성 보장 후 재시도 |