쿠폰 발급 시 발생한 동시성, 데드락 문제 해결 #28
eggnee
started this conversation in
Issue tracking
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
비관적 락 도입 배경
쿠폰 발급 시 발생한 동시성 문제와 데드락을 해결하기 위해 비관적 락을 도입하기로 했다.
비관적 락은 동시성 문제가 발생할 가능성이 높다고 가정하고, 데이터를 사용하기 전에 락을 걸어 다른 트랜잭션의 접근을 차단하는 방식이다.
락은 트랜잭션 단위로 설정되며, 커밋되거나 롤백될 때 자동으로 해제된다. (
flush()
는 락 해제와 무관)낙관적 락과 비관적 락 비교
낙관적 락
JPA에서는 버전 관리 기능을 사용하여 낙관적 락을 구현할 수 있습니다.
비관적 락
쿠폰 발급 시에는 트랜잭션 충돌이 많이 발생할 것이라 생각해서 비관적 락을 사용하기로 결정했다.
JPA에서 비관적 락 설정
JPA에서 비관적 락은
@Lock
어노테이션을 사용하여 설정할 수 있다.@Lock(LockModeType.PESSIMISTIC_READ)
: 데이터를 읽기 위해 비관적 읽기 락을 설정하여, 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 방지 (S Lock)@Lock(LockModeType.PESSIMISTIC_WRITE)
: 데이터를 수정하기 위해 비관적 쓰기 락을 설정하여, 다른 트랜잭션이 해당 데이터를 읽거나 수정하지 못하도록 방지 (X Lock)@Lock(LockModeType.OPTIMISTIC)
: 낙관적 락을 설정하여 트랜잭션이 완료되기 전에 충돌을 검증하는 방식@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
: 낙관적 락을 사용하면서, 트랜잭션이 성공하면 엔티티의 버전 필드를 강제로 증가시켜 버전 충돌 방지데드락 해결을 위한 비관적 락 적용
기존에 데드락이 발생한 이유는 서로 다른 트랜잭션이 동일한 데이터에 대해 S Lock을 설정하여, 수정에 필요한 X Lock을 제대로 걸지 못했기 때문이다.
따라서 나는 쿠폰 발급 로직에서 잔여 수량을 감소시키기 위해 쿠폰 조회 메서드에 비관적 락을 적용하기로 했다.
락을 적용하려고 했던 첫 시도
처음에는 CouponRepository 인터페이스에 비관적 락을 설정해 보았으나, 적용되지 않았다.
인터페이스에서 락을 적용하려고 했던 이유는
findById
와 같은 조회 메서드를 사용할 때 트랜잭션 간의 동시성을 관리하려는 목적이었지만, 실제로는 JPA 엔티티에 비관적 락을 걸어야 한다는 것을 깨달았다.구현 방식 수정
그래서 CouponRepositoryImpl 구현체에 비관적 락을 적용해보았다.
findById
와 같은 일반 조회 메서드에 락을 걸게 되면, 단순 조회 API 호출 시에도 락이 적용되기 때문에, 실제 업데이트용 조회 메서드를 새로 만들었다.그런데 또 다시 락이 제대로 적용되지 않았다.
문제 해결
다시 확인해 보니, JPA에서 제공하는 쿼리 메서드나
@Query
어노테이션을 사용하여 엔티티 객체에 락을 걸어야 한다는 사실을 알게 되었다.이렇게 엔티티 메서드에 비관적 락을 적용하고 나니
flush()
를 호출하지 않고도 데드락이 발생하지 않았고, 동시성 문제도 해결되었다.트랜잭션 실행 순서에 대한 오해
락을 적용한 후 실행 순서를 그림으로 나타내보려 했는데 뭔가 이해가 가지 않았다.
내가 생각한 대로 트랜잭션이 실행된다면 락을 적용해도 데드락이 해결되지 않아야 한다.
기존에 내가 생각한 트랜잭션 순서에는 SELECT를 고려하지 않고 있다.
단순히 SELECT 쿼리는 UPDATE와 같이 쿼리가 실행된다고 생각했기에
INSERT 이후 UPDATE 쿼리에서 X LOCK을 적용한다면 적용 전과 똑같은 결과가 발생한다. 즉 데드락이 발생할 것이다.
그런데 실제로
JPA X LOCK
을 적용하니 데드락이 발생하지 않았다.실제로는 SELECT 쿼리가 JPA의 쓰기 지연과 관계없이 조회 메서드 호출 시 바로 실행된다는 점을 깨달았다.
따라서 실제 쿼리 실행 순서는 다음과 같다.
flush(), X LOCK 적용 전 데드락, 동시성 문제 발생
flush() 적용하여 쿼리 실행 순서 보장
JPA 비관적 락 적용 후 모든 문제 해결
결론
@Query
어노테이션을 사용해 JPA 메서드에 직접 적용해야 한다.SELECT
는 즉시 실행되며, UPDATE는 트랜잭션 커밋 시점에 실행되어 X LOCK을 통해 동시성 문제가 해결된다.Beta Was this translation helpful? Give feedback.
All reactions