JPA와 트랜잭션 환경에서 자주 발생하는 데드락 사례 분석

운영 환경에서 가끔 이런 로그를 보게 됩니다.

ERROR: deadlock detected
Detail: Process 123 waits for ShareLock on transaction 456
 

또는 Spring 로그에서는 다음처럼 나타납니다.

org.springframework.dao.DeadlockLoserDataAccessException
 

이 에러는 DB가 동시에 실행되는 트랜잭션 사이에서 교착 상태(Deadlock)를 감지했을 때 발생합니다.


1. Deadlock이란 무엇인가

Deadlock은 두 개 이상의 트랜잭션이 서로의 Lock을 기다리며 멈춰있는 상태입니다.

Transaction A
lock row1

Transaction B
lock row2
 

이후

Transaction A → row2 필요
Transaction B → row1 필요
 

구조

A → row2 기다림
B → row1 기다림
 

결과

서로 기다리면서 무한 대기
 

DB는 이를 감지하고 한 트랜잭션을 강제로 종료합니다.


2. Deadlock이 발생하는 대표적인 사례

실무에서 가장 흔한 경우입니다.


사례 1. Update 순서가 다른 경우

테이블

account
 

데이터

id = 1
id = 2
 

트랜잭션 A

 
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
 

트랜잭션 B

 
UPDATE account SET balance = balance - 100 WHERE id = 2;
UPDATE account SET balance = balance + 100 WHERE id = 1;
 

결과

A → row1 lock
B → row2 lock
 

이후

A → row2 요청
B → row1 요청
 

Deadlock 발생.


사례 2. JPA Lazy Loading + Update

JPA에서 이런 코드가 있을 수 있습니다

 

@Transactional
public void updateOrder(Long id) {

Order order = orderRepository.findById(id);

order.getItems().size();

order.setStatus("COMPLETE");
}
 

문제

Lazy loading
 

때문에 추가 쿼리가 실행되면서 Lock 순서가 꼬일 수 있습니다.


사례 3. Batch Update

대량 업데이트 시에도 자주 발생합니다.

 
UPDATE orders SET status = 'COMPLETE'
WHERE status = 'READY'
 

동시에 여러 서버가 실행하면

row lock 충돌
 

이 발생합니다.


사례 4. FK 관계

orders
order_items
 

트랜잭션

A → orders update
B → order_items update
 

FK 검사 과정에서 lock 순서가 꼬일 수 있습니다.


3. Deadlock이 발생하면 DB는 어떻게 할까

DB는 Deadlock을 감지하면

한 트랜잭션을 강제 rollback
 

합니다.

그래서 Spring에서는 다음 예외가 발생합니다.

DeadlockLoserDataAccessException
 

4. Deadlock을 해결하는 방법

Deadlock은 완전히 없앨 수는 없지만 발생 확률을 줄일 수 있습니다.

 

방법 1. Lock 순서를 동일하게 유지

가장 중요한 방법입니다.

항상 id 순서로 update
 
 
UPDATE account SET ... WHERE id = 1;
UPDATE account SET ... WHERE id = 2;
 

모든 트랜잭션이 같은 순서를 사용해야 합니다.


방법 2. 트랜잭션을 짧게 유지

트랜잭션이 길어질수록 lock이 오래 유지됩니다.

 
@Transactional
public void process() {

externalApiCall(); // 문제
updateDB();
}
 

이 경우 API 호출 동안 DB lock이 유지됩니다.


방법 3. SELECT FOR UPDATE 사용

명시적으로 lock을 잡을 수 있습니다.

 
SELECT * FROM account
WHERE id = 1
FOR UPDATE
 

이 방법은 lock 순서를 제어할 수 있습니다.


방법 4. Retry 처리

Deadlock은 일시적인 경우가 많습니다.

그래서 보통 재시도 로직을 넣습니다.

Spring에서는

 
@Retryable(
value = DeadlockLoserDataAccessException.class,
maxAttempts = 3
)
 

같은 방식으로 처리합니다.


방법 5. 인덱스 최적화

인덱스가 없으면

table scan
 

이 발생합니다.

그러면

많은 row lock
 

이 생깁니다.

따라서 update 조건에는 반드시 index가 필요합니다.

 

 

5. Deadlock 모니터링

PostgreSQL에서는 다음 쿼리로 확인할 수 있습니다.

 
SELECT * FROM pg_locks;
 

또는

 
SELECT * FROM pg_stat_activity;
 

이 정보를 보면

blocking query
waiting query
 

를 확인할 수 있습니다.


6. 실무에서 가장 중요한 원칙

Deadlock을 줄이려면 다음 원칙을 지켜야 합니다.

항상 같은 순서로 row 접근
트랜잭션 최소화
인덱스 사용
대량 업데이트 주의
 

정리

Deadlock은 대규모 트래픽 시스템에서 언젠가는 반드시 발생하는 문제입니다.

하지만 설계 단계에서 다음을 고려하면 발생 확률을 크게 줄일 수 있습니다.

Lock 순서 통일
짧은 트랜잭션
Retry 로직
적절한 인덱스
 

✔ 핵심 한 줄

Deadlock은 버그가 아니라 동시성 설계 문제다.
LIST

+ Recent posts