최근 JPA 관련해서 특이한 에러가 발생하였다.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE
);
@Entity
@Table(name = "users")
// ...
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
email에 유니크 제약조건이 걸려있음을 유의하자.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
// ...
@Transactional
public void deleteAndInsertWithSameEmail(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
String email = user.getEmail();
userRepository.delete(user);
// 동일한 email을 가진 새로운 User 객체 생성 후 저장
User newUser = new User();
newUser.setEmail(email);
userRepository.save(newUser); // 여기에서 에러가 발생한다.
}
}
위와 같은 로직을 실행시키면 Unique 제약조건을 어겼다며 에러가 발생한다. (손코딩이라 로직 흐름만 참고)
이상한 일이다.
1. 분명이 기존에 레코드를 삭제했으므로, email이 겹칠 일이 없는데 어떻게 email이 겹친단 말인가
2. 설령 제약조건에 걸린다고 하더라도, 에러 발생지점이 save라는 점도 특이하다.
3. flush를 하지 않았으므로, transaction aop의 커밋시점에 에러가 발생했어야 한다.
처음에는 flush 시점에 뭔가 delete문이 나중에 나가는 문제로 해당 에러가 발생했다고 생각했지만, 위 2,3번 사항이 맞지 않았다.
@GeneratedValue(strategy = GenerationType.IDENTITY)
엔티티의 키 생성 전략이 GenerationType.IDENTITY
라는 점이다.
키 생성 전략에는 여러가지가 있지만 그 중 IDENTITY의 경우에는 db 자체의 auto increment key를 사용한다.
(hibernate에서 mysql 키 전략 기본값이 변경된 것 같다 -> 참고)
따라서 save시 엔티티에 대한 insert가 우선 이루어지고 키를 배정받게 된다.
참고
# save시 동작 방식 function save(entity): if (entity.id == null): // ID가 없으면 새 엔티티로 판단 persist(entity) // INSERT 실행 return entity else: if (1차 캐시에 entity.id 존재?): // 동일 ID의 엔티티가 캐시에 있는지 확인 merge(1차 캐시의 엔티티, entity) // 캐시의 엔티티를 수정 (변경 감지) else: dbEntity = findById(entity.id) // DB 조회 if (dbEntity 존재?): merge(dbEntity, entity) // 기존 엔티티 업데이트 (변경 감지 후 UPDATE 실행) else: persist(entity) // DB에도 없으면 새 엔티티로 판단하고 INSERT 실행 return entity
GenerationType 동작 방식 IDENTITY
즉시 INSERT
실행 후 ID 할당SEQUENCE
시퀀스에서 ID를 미리 가져오고, 트랜잭션 커밋 시 INSERT
TABLE
별도 키 테이블에서 ID를 가져오고, 트랜잭션 커밋 시 INSERT
어쨌든 내가 생각했을 때는,
내가 지금 생각했을 때 가능할 것 같은 시나리오는
이 정도 인것 같은데, 추가적인 시나리오가 생각나는 경우 댓글로 알려주면 좋겠다.
안녕하세요 유알님, Rock 관련 포스트를 읽다가 글이 너무 좋아 여기까지 찾아왔네요.
User delete 시 soft delete 방식을 채택하고 있다면, delete 전 user.email()을 'deleted' + email로 update하여 이메일이 같은 새로운 User를 insert를 할 수 있다고 생각합니다. 다만, 벌크 연산을 언급하신 것을 보았을 때 해당 로직이 많이 호출될 것 같아서 무거워질수도? 있겠네요..
아마 당연하게도 유알님께서 위 방법을 생각하셨겠지만 뉴비의 생각을 한번 올려봅니다ㅎ