Home
27. 데이터베이스 락 이해하기DEV
26. Spring AOP CGLIB 프록시 핵심 정리DEV
25. 익명 댓글 커서 페이징 운영가이드DEV19
23. slug 기반 동적 라우팅 운영 정리DEV
22. Vite to Next 마이그레이션 정리DEV
20. Broken HTTPS 이슈 트래킹DEV
19. Spring 커스텀 어노테이션DEV
18. 홈랩 컨테이너 서비스 운영 WORK LOGDEV
17. 홈랩 컨테이너 서비스 운영 ROAD MAPDEV
16. 브라우저 히스토리 상태 복원DEV
page

데이터베이스 락 이해하기

목차

  • 목적
  • 왜 락이 필요한가
  • 락 기본 종류
  • 락 범위
  • 격리수준과 락 관계
  • 비관적 락 vs 낙관적 락
  • 데드락 실무 대응
  • Spring/JPA 적용 포인트
  • 선택 가이드
  • 빠른 선택 표
  • 체크리스트
  • md_doc 테이블 기준 실전 예제

1. 목적

  • 동시성 환경에서 데이터 정합성을 지키기 위한 락의 핵심 개념 정리
  • 트랜잭션 격리수준, 비관적/낙관적 락, 데드락 대응까지 실무 관점으로 연결
  • Spring/JPA 기준으로 바로 적용 가능한 선택 기준 제공

2. 왜 락이 필요한가

동시 수정 구간의 대표 문제:

  • Lost Update: 마지막 저장이 이전 변경을 덮어씀
  • Dirty Read: 커밋되지 않은 값을 읽음
  • Non-Repeatable Read: 같은 조회인데 값이 바뀜
  • Phantom Read: 같은 조건 재조회 시 행 수가 바뀜

md_doc 기준 문제 예시:

항목락/격리 제어 부족 상황실제 데이터 문제
Lost UpdateA, B가 md_doc_id=10을 조회 후 각각 title 수정 저장마지막 커밋이 먼저 커밋한 값 덮어씀 (A 제목 소실)
Dirty ReadA가 status=1 -> 9로 변경 후 미커밋, B가 그 값 조회A 롤백 시 B가 존재하지 않는 상태값 기반 로직 실행
Non-Repeatable ReadA 트랜잭션에서 md_doc_id=11을 두 번 조회, 사이에 B가 content 수정 커밋같은 트랜잭션 내 1차/2차 조회 결과 불일치
Phantom ReadA가 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 ASession B결과대표 에러
같은 md_doc_id 동시 UPDATEUPDATE ... WHERE md_doc_id=? 후 미커밋같은 PK UPDATEB 대기 후 A 커밋 시 진행1205 가능
FOR UPDATE 선점 후 수정SELECT ... FOR UPDATE같은 행 UPDATEB 대기1205 가능
범위 FOR UPDATE + INSERTstatus/sort_order 범위 잠금해당 범위로 INSERTINSERT 대기1205 가능
반대 순서 다건 수정20 -> 21 순서 UPDATE21 -> 20 순서 UPDATE데드락 감지, 한쪽 롤백1213

12.7 에러코드 대응 표

에러 코드의미1차 대응애플리케이션 전략
1205락 대기 시간 초과잠금 트랜잭션 길이 단축, 인덱스 점검짧은 backoff 후 제한 재시도
1213데드락으로 강제 롤백잠금 순서 통일, 트랜잭션 축소멱등성 보장 후 재시도

댓글

0개
첫 댓글을 남겨보세요.