| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- katalon xpath
- oracle group by
- CSTS 폭포수 모델
- 주식 양도세 신고방법
- git 연동
- 피보나치함수
- 국세청 해외주식 양도세 신고방식
- tomcat log
- 테스트 자동화
- 홈택스 해외주식 양도세
- 재귀 예제
- katalon 사용법
- 한국투자증권 해외주식 양도세
- 한국투자증권 양도세 신고
- 피보나치함수 예제
- 해외주식 양도세 신고
- katalon 비교
- java.sql.SQLSyntaxErrorException
- javascript 자동완성
- 피보나치 예제
- 최대공약수 예제
- katalon
- js 자동완성
- 해외증권 양도세 한국투자증권
- recursion example
- Katalon Recorder 사용법
- katalon 자동화
- bfs 미로탐색 java
- 재귀함수 예제
- 톰캣 실시간 로그
- Today
- Total
엄지월드
JPA 동시성 제어 주의점(ConstraintViolationException 에러) 본문
아래와 같이 ConstraintViolationException 에러가 발생했다.
문제를 확인해보니 특정 엔티티를 업데이트하거나 삭제하려 할 때, 데이터베이스에 저장된 해당 엔티티의 상태가 애플리케이션이 엔티티를 로드했을 때와 달라졌을 때 발생하는 것이었다. 즉, 다른 트랜잭션에 의해 이미 변경되었거나 삭제되었다는 뜻인 것이다.
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement [(conn=1338) Duplicate entry 'ejy1024@-ejy1024@_test1234@_...' for key 'PRIMARY'] [/* insert for com.connect.service.chatting.entity.RoomMembership */insert into room_memberships (joined_at,room_name,room_id,user_id) values (?,?,?,?)]
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.connect.service.chatting.entity.RoomMembership#RoomMembershipId(userId=ejy1024, roomId=ejy1024test12341766307683336)]
코드로 보자면 existsById로 확인하는 시점과 save를 호출하는 시점 사이에 다른 트랜잭션이 먼저 동일한 기본 키로 RoomMembership 엔티티를 삽입했을 때 발생할 수 있다.
val exists = roomMembershipRepository.existsById(membershipId) // 해당 유저가 이미 이 방의 멤버인지 DB에서 확인
if (!exists) { // 아직 멤버가 아니라면
val newMembership = RoomMembership(
id = membershipId, // 복합 키
chatRoom = chatRoom, // 관계 매핑된 ChatRoom 엔티티
roomName = roomName,
joinedAt = LocalDateTime.now() // 참여 시간 기록
)
roomMembershipRepository.save(newMembership) // 새로운 멤버십을 DB에 저장
return true // 성공적으로 새로 추가됨을 반환
}
순서는 이렇다.
- 트랜잭션 A : roomMembershipRepository.existsById(membershipId)를 호출한다.
이 시점에는 해당 RoomMembership이 DB에 없어서 exists는 false를 반환한다. 그래서 if (!exists) 블록으로 진입한다. - 트랜잭션 B (거의 동시에): 트랜잭션 A가 save를 호출하기 전에, 트랜잭션 B가 먼저 같은 membershipId를 가진 RoomMembership을 생성하고 DB에 INSERT하는 데 성공한다.
- 트랜잭션 A (다시): 이제 트랜잭션 A가 roomMembershipRepository.save(newMembership)를 호출하여 DB에 INSERT를 시도한다.
- 에러 발생: 하지만 이미 트랜잭션 B가 동일한 PRIMARY KEY를 가진 RoomMembership을 삽입했으므로, DB는 PRIMARY KEY 제약 조건 위반을 감지하고 Duplicate entry '...' for key 'PRIMARY' 에러를 발생시킨다. 하이버네이트는 이를 ConstraintViolationException으로 변환하여 던지게 된다.
해결을 위해서는 아래와 같이 existsById로 먼저 확인하고 save하는 방식(read-then-write) 대신, findById를 사용하여 엔티티의 존재 여부를 확실히 확인하고 그 결과에 따라 분기 처리하는 것이 가장 안정적이라고 한다.
이유는 existsById는 단순히 존재 여부만 확인하지만, findById는 실제 엔티티를 영속성 컨텍스트로 가져오려고 시도하기 때문이다.
다시 말하면 findById는 단순히 데이터베이스를 조회하는 것을 넘어, JPA/하이버네이트의 영속성 컨텍스트(Persistence Context)와 연동하여 동작한다.
만약 현재 트랜잭션 내에서 동일한 ID를 가진 엔티티가 이미 로드되어 영속성 컨텍스트에 관리되고 있다면, DB 쿼리 없이 해당 엔티티를 즉시 반환하게 된다.
val exists = roomMembershipRepository.findById(membershipId) // 해당 유저가 이미 이 방의 멤버인지 DB에서 확인
if (exists.isEmpty) { // 아직 멤버가 아니라면
val newMembership = RoomMembership(
id = membershipId, // 복합 키
chatRoom = chatRoom, // 관계 매핑된 ChatRoom 엔티티
roomName = roomName,
joinedAt = LocalDateTime.now() // 참여 시간 기록
)
roomMembershipRepository.save(newMembership) // 새로운 멤버십을 DB에 저장
return true // 성공적으로 새로 추가됨을 반환
}
return false // 이미 존재하는 멤버임을 반환
결론
[이전]
roomMembershipRepository.existsById(membershipId)
[이후]
roomMembershipRepository.findById(membershipId)
1. Race Condition 발생 가능성이 있는 "불필요한 INSERT 시도"를 더 효율적으로 차단한다. (이미 존재한다면 findById가 엔티티를 반환하므로, if (isEmpty) 로직에 의해 save가 호출될 기회 자체가 사라진다)
2. 하이버네이트의 영속성 컨텍스트와 더 깊이 통합되어 관리된다. (엔티티 객체 자체를 가져오기 때문에)
혹은 추가적으로 아래와 같이 낙관적 잠금을 활용해도 된다.
// ChatRoom.kt
@Entity
@Table(name = "chat_room")
class ChatRoom(
@Id
val roomId: String,
// ... 다른 필드들 ...
@Version // 이 부분을 추가 (Long 타입 필드를 선언하고 @Version 어노테이션 추가)
var version: Long? = null,
) {
// ...
'java > Spring' 카테고리의 다른 글
| 지연 초기화 문제(Initializing Spring DispatcherServlet) (1) | 2025.06.30 |
|---|---|
| Spring Data JPA 메서드 명명 규칙 (0) | 2025.06.03 |
| [Spring] 이메일 전송 구현(구글 이메일) (2) | 2025.01.26 |
| @EnableConfigServer가 import 안되는 현상 (Spring Cloud BOM 적용) (0) | 2024.07.06 |
| Spring Boot JPA 실행 오류 (Error creating bean with name 'entityManagerFactory' defined in class path resource) (0) | 2024.05.04 |