Server/Spring JPA

Spring Data JPA - Bulk Update(일괄 수정)할 때 주의할 점! [영속성 컨텍스트]

JaeHoney 2022. 4. 8. 22:38

Bulk Update

JPA에서는 변경 감지를 통해 엔터티를 수정할 수 있지만, 그러면 각 엔터티별로 쿼리가 나가게 됩니다.

 

그래서 변경할 레코드들의 SET 절이 동일하다면 일괄 수정을 하는 것이 좋습니다.

 

가령, 만약 모든 사용자에게  10000원의 리워드를 제공하겠다고 하면 1개의 쿼리로 전체 업데이트를 할 수 있습니다.

UPDATE user SET point = point + 10000;

 

@Modifying

Spring Data JPA에서는 벌크 업데이트를 위해서 Modifying 애노테이션을 사용해야 합니다.

 

SpringDataJPA가 해당 리포지토리 메소드가 SELECT문인지 UPDATE문인지 알아야 Return Type을 결정하는데, 아직은 내부적으로 Query만 보고 판단을 못해서, 명시를 해줘야 Return type을 결정하기 때문입니다.

@Modifying
@Query("update User u set u.point = m.point + :reward")
int plusPointAll(@Param("reward") int reward);

 

Persistence

JPA에서 Bulk update를 할 때 주의할 사항이 있습니다.

 

아래 예시를 보시면, Point가 2천인 유저와 5천인 유저를 생성한 후 Point를 10000씩 더했습니다. 그리고 Point가 2000인 유저를 조회하는데, 그대로 Point가 2천이 나옵니다.(?) 그럼 장애가 터지겠죠.

// (1)
userRepository.save(new User("user1", 2000));
userRepository.save(new User("user2", 5000));
// (2)
userRepsitory.plusPointAll(10000);
// (3)
User result = userRepository.findByUsername("user1");

System.out.println(result.getPoint()); // -> 2000

 

기이한 현상의 이유는 1차 캐시라고 불리는 영속성 컨텍스트 때문입니다.

 

일괄 수정(BulkUpdate)은 단일 Update와 다르게, 영속성 컨텍스트에 있는 엔터티를 변경하지 않습니다. 

 

내부적인 동작은 아래와 같이 동작합니다.

  1. 영속성 컨텍스트에 엔터티 2개를 추가 + 쓰기 지연 SQL 저장소에 INSERT문 저장
  2. 영속성 컨텍스트를 flush(JPQL 실행 때문) -> 쓰기 지연 SQL 저장소에만 UPDATE문 저장
  3. 영속성 컨텍스트에서 엔터티 조회
  4. Update문이 적용되지 않은 값을 반환

 

해결 방법

Bulk Update로 인해 변경된 값을 참조해야 한다면, 영속성 컨텍스트를 DB에 반영하고 영속성 컨텍스트를 비워야 합니다.

 

그러면, 해당 값을 참조할 때 영속성 컨텍스트가 아니라 DB에 직접 접근하게 되어서 반영된 값을 가져올 수 있습니다.

userRepsitory.plusPointAll(10000);
entityManager.flush();
entityManager.clear();

 

@Modifying의 clearAutomatically 옵션을 사용해서 처리하는 방법도 있습니다!

@Modifying(clearAutomatically = true)
@Query("update User u set u.point = m.point + :reward")
int plusPointAll(@Param("reward") int reward);

감사합니다.