티스토리 뷰

서론
사용자가 많아져 서비스가 커질수록 다수의 트랜잭션이 동시에 실행되는 상황에서 컨트롤하는 건 쉽지 않아 질 것입니다.
그럼 저희는 성능과 데이터 일관성 사이에서 끊임 없는 트레이드 오프를 하게 될 것입니다.
예를 들어,
커머스 도메인에 상품 재고는 1개인데 2명의 사용자가 같은 상품을 주문하려고 하면 어떻게 될까요?
상식적으로 생각하면 1명은 구매 성공, 1명은 구매 실패일 것입니다.
하지만 재고는 -1개에다가 2명이 모두 주문이 되는 이상한 현상이 나타날 것입니다.
이 문제를 해결하기 위해 LOCK 에 대해 알아보려고 합니다.

LOCK은 무엇인가?
DB에서의 락(LOCK)은 여러 사용자가 동시에 같은 데이터에 접근할 때 데이터의 일관성과 무결성을 유지하는 즉, 동시성 제어를 위해 사용하는 필수적인 기술입니다.
동시성 제어를 안하여 동시에 여러 트랜잭션이 수행된다면 더티 리드, 반복 불가능한 리드, 팬텀 리드 등 다양한 문제 현상이 발생할 수 있기 때문입니다.
LOCK의 동작 원리 예시






LOCK을 어떻게 걸까에 대한 전략
- 낙관적 락 (Optimistic Locking) : 데이터에 대한 변경이 드물게 발생하고, 충돌 가능성이 낮은 환경에서 사용되는 전략입니다.
- 비관적 락 (Pessimistic Locking) : 데이터에 대한 변경이 자주 발생하고, 충돌 가능성이 높은 환경에서 사용되는 전략입니다.
낙관적 락(Optimistic Locking)의 특징
- "충돌은 잘 일어나지 않는다" 라고 낙관적으로 가정하고 DB 데이터에 LOCK을 걸지 않는 방식입니다.
- 대표적으로 Row 데이터에 버전 정보(Version)를 가지고 있다가 저장할 때 버전이 바뀌지 않았는지 확인해서 충돌을 감지하는 방법이 있습니다.
- 락을 사용 하지 않아 상대적으로 성능이 좋고, 데드락 걱정이 없습니다.
- 충돌이 발생하면 OptimisticLockException, ObjectOptimisticLockingFailureException 예외를 잡고 자신의 서비스에 맞춰 비즈니스를 녹여내면 될 것 같습니다.
예시로, 서버에서 Retry를 한다 던지, 사용자에게 이미 수정된 글이라 다시 해달라는지..?
// 버전 정보 사용하는 엔티티
@Entity
public class Product {
@Id
private Long id;
@Version
private int version;
private String name;
}
-- JPA 내부적으로 사용하는 쿼리
UPDATE ... WHERE id=? AND version=?
+ Mybatis로 사용할 때
<!-- Mybatis 사용 예시 -->
<update id="updateProductWithVersion">
UPDATE product
SET name = #{name},
price = #{price},
version = version + 1
WHERE id = #{id}
AND version = #{version}
</update>
비관적 락(Pessimistic Locking)의 특징
- "언젠간 충돌이 일어날 거야" 라고 비관적으로 생각하고 DB 데이터에 LOCK을 걸어버리는 방식입니다.
- 데이터를 읽거나 쓸 때 즉시 락을 획득해서, 다른 트랜잭션이 접근하지 못하도록 막습니다.
- 충돌 가능성이 높은 환경에 사용해도 데이터 정합성을 보장합니다.
- 락(LOCK)을 보유하는 시간에 따라 데드락(DeadLock)이 발생하거나 다른 작업에 영향을 줄 수 있습니다.
// JPA 락을 거는 예시 코드
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findByIdWithPessimisticLock(@Param("id") Long id);
Tip) 비관전 락 전략에 대한 락의 2가지 종류는 아래에 자세하게 작성하도록 하겠습니다.
비관적 락에 사용되는 LOCK의 종류
- 공유 락(Shared Lock) : 공유 락은 데이터를 읽을 때 사용됩니다. 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있지만, 데이터를 수정하려는 트랜잭션은 대기를 해야 합니다.
즉, 읽기 전용 작업에 적합한 락입니다. - 배타 락(Exclusive Lock) : 배타 락은 데이터를 수정할 때 사용됩니다. 데이터를 수정하는 트랜잭션은 해당 데이터를 다른 트랜잭션이 접근하지 못하도록 잠급니다. 그래서 배타 락을 사용하는 동안에는 다른 트랜잭션이 해당 데이터를 읽거나 수정할 수 없습니다.
공유 락 (Shared Lock)
// 스프링에서 공유락 획득
// 주의) 트랜잭션 범위 내에서만 유효
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findWithSharedLock(@Param("id") Long id);
-- 공유락 획득
SELECT * FROM product WHERE id = 1 FOR SHARE;
* 타임아웃이 없다는 가정

배타 락 (Exclusive Lock)
// 스프링에서 배타 락 획득
// 주의) 트랜잭션 범위 내에서만 유효
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findWithExclusiveLock(@Param("id") Long id);
-- 배타락 획득 (쓰기 전용 락)
SELECT * FROM product WHERE id = 1 FOR UPDATE;
* 타임아웃이 없다는 가정

락 호환 상태 표
| 요청 ↓ \ 상태 → | 공유 락 걸려 있는 상태 | 배타 락 걸려 있는 상태 |
| 읽기 요청 | 허용 | 대기 |
| 쓰기 요청 | 대기 | 대기 |
정리하자면, 공유 락이라면 읽기만 가능하고 배타 락이라면 읽기, 쓰기 둘 다 안되고 대기해야 합니다.
LOCK의 문제점과 해결 방안
- 배타 락(LOCK)이 너무 자주 사용되거나 오랫동안 유지되면, 시스템의 처리량이 감소하고 대기 시간이 증가할 수 있습니다.
- 예를 들어, 한 트랜잭션이 자원을 락(LOCK)한 상태로 오랫동안 유지되면, 다른 트랜잭션들은 그 자원을 기다려야 하므로 전체 시스템의 성능이 저하됩니다.
- 이러한 예시로 데이터 베이스 배타 락(LOCK) 관리의 중요성은 시스템의 성능에 큰 영향을 미치는 것을 알 수 있습니다.
- 잘못된 배타 락 (LOCK) 관리로 인해 생기는 다양한 현상인 데드락(Deadlock), 롱 락(Long Lock), 그리고 락 경합(Lock Contention) 문제를 알아보고, 이를 해결하기 위한 전략을 알아보겠습니다.
데드락 (Deadlock)
- 데드락은 두 개 이상의 트랜잭션이 서로의 락(LOCK) 을 기다리며 무한 대기 상태에 빠지는 현상입니다.
- 예를 들어, 트랜잭션 T1이 자원 1을 잠그고 자원 2를 기다리며, 트랜잭션 T2는 자원 2를 잠그고 자원 1을 기다린다면 두 트랜잭션은 서로를 무한히 기다리게 됩니다.
- 이로 인해 시스템 자원이 비효율적으로 사용되며, 트랜잭션의 처리 속도가 크게 저하가 될 것입니다.
| 트랜잭션 T1 | 트랜잭션 T2 |
| BEGIN; UPDATE product SET price = price + 100 WHERE id = 1; |
|
| BEGIN; UPDATE product SET price = price * 1.1 WHERE id = 2; |
|
| UPDATE product SET price = price + 100 WHERE id = 2; ...(대기 상태)... |
|
| UPDATE product SET price = price * 1.1 WHERE id = 1; ...(대기 상태)... |
|
| Commit 하려고 해도 교착 상태로 인해 롤백! | Commit 완료 |

데드 락(Deadlock)의 해결 방법으로는 애플리케이션 수준에서 락을 획득하는 순서를 일관되게 유지함으로써 데드락을 방지할 수 있습니다.
예를 들어, 트랜잭션 T1, T2는 항상 자원 1을 먼저 락(LOCK)을 획득 한 후 자원 2를 락(LOCK)을 획득하는 방식으로 순서를 정하면, 데드락(Deadlock)을 예방할 수 있습니다.
롱 락 (Long Lock)
- 롱 락은 특정 트랜잭션이 락(LOCK)을 오랫동안 유지하여 다른 트랜잭션들을 기다리게 만드는 현상입니다.

롱 락(Long Lock)의 해결 방법으로는 트랜잭션의 실행 시간을 가능한 짧게 유지하고, 불필요한 락을 최소화하는 방법과 필요한 자원만을 락하여, 다른 트랜잭션이 필요 없는 자원을 기다리는 상황을 방지하는 방법으로 예방할 수 있습니다.
락 경합 (Lock Contention)
- 여러 트랜잭션이 동시에 같은 자원을 락을 획득하려고 할 때 발생하는 현상입니다.
- 그중에 1개의 트랜잭션만 락을 얻고 나머지는 대기 상태가 되기 때문에 성능 저하가 발생합니다.
최악의 스레드는 무한 로딩 같은 현상이 벌어질 것입니다.
락 경합을 줄이는 방법으로는 1. 락 대상 자원을 나누는 테이블 파티셔닝으로 경합을 분산하거나 2. 꼭 필요한 순간에만 Row-Level로 된 데이터만 락을 획득하는 방법이 있습니다.
추가로 공부해야 할 것들
- 배타 락을 정말 걸어야 할까?
- 공유 락은 읽기만 필요할 때 효율적인가?
- 락 경합 발생 시 어떻게 대응할 것인가?
- 락을 사용하는 대신 대체 가능한 전략이 있는가?
- 배타 락이 걸렸을 때 어떤 쿼리가 풀스캔하여 조건 조회 될 때는 어떻게 되는가?
- 인덱스로 데이터를 조회 중일 때도 락 걸려 있으면 대기가 되는가?
- 락이 병목을 만든다면 트랜잭션 설계를 바꿔야 하지 않나?
- 낙관적 락이 실패했을 때 재시도 로직을 어떻게 설계할 것인가?
- 분산 락을 사용할 경우 장애 복구, 타임아웃, 락 만료 처리 로직을 신중하게 설계하고 있는가?

감사합니다.
'데이터 베이스 > 🐘PostgreSQL' 카테고리의 다른 글
| [PostgreSQL] 로컬을 넘어 운영으로: Master-Slave 복제 3대 장애 시나리오와 방어책 (0) | 2026.04.20 |
|---|---|
| [PostgreSQL] 대규모 트래픽을 견디는 아키텍처 : Primary-Replica 구축부터 WAL 동기화까지 (1) | 2026.04.15 |
| [PostgreSQL] 운영에 필요한 로그 세팅 6가지 (0) | 2025.05.06 |
| [PostgreSQL] 트랜잭션과 격리 레벨 : 개발자가 꼭 알아야 할 개념 (0) | 2025.04.22 |
- Total
- Today
- Yesterday
- spring
- 알고리즘
- 카카오 로그인
- Fetch
- BFS
- Front
- 멀티모듈
- 데이터 베이스
- 개발자
- Spring Security
- java
- 개발
- Primary-Replica
- DBeaver
- JPA 페이징
- 디자인패턴
- 프로세스
- 장애대응
- 네트워크
- 트랜잭션
- 시간 객체
- 개발환경
- CI/CD
- Flutter
- JavaScript
- aws
- 깃허브 액션
- 코딩테스트
- 개발블로그
- 그리디
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |