본문 바로가기
Spring/JPA

JPA 동일 트랜잭션에서 update와 insert 동시에 수행할 때 문제 해결하기 (쿼리 실행 순서 문제)

by 흑시바 2023. 2. 5.

unique 제약이 걸린 필드를 '동일한 트랜잭션' 내에서 수정하고 저장해야 하는 경우가 있었다.

그래서 필드를 먼저 수정(update)하고 새롭게 저장(insert)하면 순서대로 처리되지 않을까? 예상하며 코드를 작성했다.

하지만 예상과 다르게 예외가 발생하며 실패했고, 이후 원인을 찾아서 해결하게 되었다.

 

이 문제를 해결했던 내용을 비슷한 상황을 재현한 간단한 테스트 코드를 활용해 공유하고자 한다.

🐕 테스트 환경

Shiba.java (엔티티 내용 중 일부)

...
    
@Column(unique = true)
private String identification;

public void updateIdentification(String identification) {
    this.identification = identification;
}
    
...

 

DB에 들어있는 값

identification 필드에 uk 제약이 있으며, 123456789 값을 가지고 있다.

 

identification은 unique 제약을 가진 필드이다. 해당 필드에 중복된 값을 보유할 수 없다.

 

만약, 특정 시바견의 identification의 번호가 변경되거나 말소되면서

동시에 다른 시바견이 해당 identification을 보유해야 하는 상황이 생기면 어떻게 해야 할까?

 

Test Code

@Test
@Transactional
void UniqueKeyExceptionTest() {
    Shiba shiba = shibaRepository.findById(1L).get();
        
    shiba.updateIdentification("11233333");

    Shiba newShiba2 = new Shiba("테스트 흑시바2", 5, Adopt.Y, "123456789");
    shibaRepository.save(newShiba2);
}

 

👉 기대하는 흐름 

 

A엔티티 findById    ➡    update        B 엔티티 insert

 

메서드 종료와 함께 트랜잭션도 종료되고 Commit이 되면서 수정 업데이트 이후 생성(insert)에 성공할 거라 기대한다.

 

💀 하지만 실제로는 SQLIntegrityConstraintViolationException이 발생하며 실패하게 된다.

SQLIntegrityConstraintViolationException 발생

 

👉 실제로 발생하는 흐름

 

A엔티티 findById    ➡    update        SQLIntegrityConstraintViolationException        B 엔티티 insert

 


🐕 원인

왜 이런 문제가 발생할까?

 

이유는 바로, '쿼리 동작 우선순위'에 있다.

Hibernate는 영속성 콘텍스트에 등록된 쿼리에 대해 하기 우선순위에 따라 쿼리가 실행되도록 설정되어 있다.

 

쿼리 실행 순서는 다음과 같다.

 

1. Inserts, in the order they were performed
2. Updates
3. Deletion of collection elements
4. Insertion of collection elements
5. Deletes, in the order they were performed

 

따라서, Insert가 Update보다 우선순위를 가지기 때문에 먼저 실행된다.

 

로그 출력 순서를 보면, update 문이 나간 것 없이 insert 쿼리만 발생하고 즉시 예외가 발생했음을 알 수 있다.

 

@Test
@Transactional
void UniqueKeyExceptionTest() {
    Shiba shiba = shibaRepository.findById(1L).get();

    shiba.updateIdentification("11233333");

    Shiba newShiba2 = new Shiba("테스트 흑시바2", 5, Adopt.Y, "1234567810");
    shibaRepository.save(newShiba2);
}

 

실제로 정상적으로 실행되도록 identification을 수정해서 테스트하면 insert 이후 update가 출력되는 것을 확인할 수 있다.

 

insert 이후 update가 출력된다.

 


🐕 해결

이런 경우, 영속성 콘텍스트의 변경 내용을 데이터베이스에 즉시 반영할 수 있도록 Repository 혹은 EntityManager의 flush() 메서드를 호출해서 문제를 해결할 수 있다.

 

flush가 발생하면 쓰기 지연 저장소에 저장된 SQL이 데이버테이스로 전송하여 변경 사항을 DB와 동기화한다.

그러므로 save 시점에는 이미 기존 identification(unique) 필드의 값이 업데이트 되어 저장에 성공하게 된다.

 

@Test
@Transactional
void UniqueKeyExceptionTest() {
    Shiba shiba = shibaRepository.findById(1L).get();

    shiba.updateIdentification("11233333");
    shibaRepository.flush();

    Shiba newShiba2 = new Shiba("테스트 흑시바2", 5, Adopt.Y, "123456789");
    shibaRepository.save(newShiba2);
}

 

실제로 쿼리도 update, insert 순으로 호출되는 것을 확인할 수 있다.

(디버깅해보면, flush() 호출 즉시 update 쿼리를 확인할 수 있다.)

 

댓글