OptimisticLockException: Row was updated or deleted by another transaction(or unsaved-value mapping was incorrect)
JPA에서 save() 호출 시 위와 같은 Exception이 발생했다.
내 경우에는 Hibernate 버전에 따른 낙관적 락킹 관련된 처리 방식의 변화 때문에 발생한 Exception이었다.
내가 의도했던 건 데이터 등록 시 ID 값을 넘겨준다면 해당 값으로 ID가 저장되고, ID 값을 주지 않으면 UUID로 생성되게 하려는 것이었는데, Spring Boot 3.4 이전까지는 이게 잘 작동이 되었다.
그런데 Spring Boot 3.4 이후부터가 문제였다. ID값을 주지 않으면 UUID로 ID를 생성하는 건 잘되지만, 내가 ID 값을 줄경우
OptimisticLockException: Row was updated or deleted by another transaction(or unsaved-value mapping was incorrect) 해당 Exception이 발생했다.
이전에는 ID값이 있으면 해당 ID 값이 존재하는지 DB에서 조회를 해서 존재하면 update, 존재하지않면 insert를 하는 방식이었는데 Hibernate 6.6 버전부터는 ID값이 주어지면 DB에서 조회를 하고, 데이터가 없다면 다른 transaction에 의해 삭제되었다고 판단해서 Exception을 발생시킨다는 것.
그래서 내가 할 수 있는건 Spring Boot 버전을 낮추거나, ID 값을 null로 주거나, @GeneratedValue를 제거하는 것이었다.
나는 @GeneratedValue를 제거하고 ID를 직접 관리하는 방식을 택했다.
@PrePersist를 사용해서 ID가 없는 경우 UUID를 자동 생성하도록 처리하도록.
✅ Hibernate 6.6 이후 ObjectOptimisticLockingFailureException이 발생하는 이유
Spring Boot 3.4부터 Hibernate 6.6이 기본적으로 사용되며, 이 버전에서는 낙관적 락킹(Optimistic Locking)과 관련된 처리 방식이 변경되었다.
🔹 낙관적 락킹(Optimistic Locking)이란?
- 동시에 여러 트랜잭션이 같은 데이터를 수정할 수 있도록 허용하지만, 최종적으로 저장할 때 데이터가 변경되지 않았는지를 확인하는 방식
- @Version 필드를 사용하여 데이터가 변경되었는지를 체크
- 즉, 한 트랜잭션에서 데이터를 조회한 후 변경하는 동안, 다른 트랜잭션이 먼저 해당 데이터를 변경했다면 예외를 발생시킴
- 충돌이 감지되었을 때 예외(OptimisticLockException)가 발생하며, 이를 통해 데이터 정합성을 유지할 수 있음
🔹 Hibernate 6.6 이전과 이후의 차이
✅ Hibernate 6.6 이전 버전
- merge()를 호출했을 때, 해당 엔티티가 데이터베이스에 존재하지 않으면 새로운 데이터를 insert 함.
- 따라서, 어떤 엔티티가 이미 삭제된 상태이더라도 merge()를 호출하면 새롭게 데이터가 삽입될 수 있었음.
✅ Hibernate 6.6 이후 버전
- merge() 또는 save() 시, 데이터베이스에서 해당 ID를 가진 행(row)이 존재하지 않으면 OptimisticLockException을 발생시킴.
- 예상치 못한 데이터 삽입을 방지하고, 낙관적 락킹의 원칙을 준수하기 위해 도입된 변경 사항.
- 특히, 엔티티가 @Id가 @GeneratedValue로 지정되었거나, @Version 필드를 가지고 있는 경우, Hibernate는 해당 엔티티가 기존에 존재했으나 삭제된 것으로 간주하고 OptimisticLockException을 발생시킴.
🔹 정리
1️⃣ Hibernate 6.6에서는 ID가 제공되었으나 데이터베이스에 존재하지 않으면 OptimisticLockException을 발생시킴.
2️⃣ save() 시에도 Hibernate가 ID 존재 여부를 확인하기 때문에, 존재하지 않는 ID를 저장하려고 하면 ObjectOptimisticLockingFailureException이 발생할 수 있음.
3️⃣ 해결 방법은 ID를 직접 설정하는 경우 @GeneratedValue를 제거하거나, 존재 여부를 확인한 후 저장하는 방식이 있음.
4️⃣ 낙관적 락킹을 활용하려면 @Version 필드를 추가하여 변경 충돌을 감지하는 방식이 필요함.
'프로그래밍 > spring boot' 카테고리의 다른 글
[스프링부트] 실전! 스프링 부트와 JPA 활용2 : 스프링 데이터 JPA, queryDSL (0) | 2023.11.26 |
---|---|
[스프링부트] 실전! 스프링 부트와 JPA 활용2 : OSIV와 성능 최적화 (0) | 2023.11.26 |
[스프링부트] 실전! 스프링 부트와 JPA 활용2 컬렉션 조회 최적화 #5 JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화 (2) | 2023.11.26 |
[스프링부트] 실전! 스프링 부트와 JPA 활용2 컬렉션 조회 최적화 #4 JPA에서 DTO 직접 조회 (1) | 2023.11.26 |
[스프링부트] 실전! 스프링 부트와 JPA 활용2 컬렉션 조회 최적화 #3.1 페이징과 한계 돌파 (2) | 2023.11.26 |