[JPA] 벌크연산와 영속성 컨텍스트는 별개다?
벌크 연산
벌크 연산이란 1개 이상의 데이터를 update 할 때 사용합니다.
벌크 연산 사용 X
식별자를 통해 Entity를 조회하여 변경 감지(Dirty Checking)를 사용합니다. 따라서 식별자에 해당하는 1개의 데이터에만 update가 수행됩니다.
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public void update(Member member, Team team) {
Member findMember = em.find(Member.class, member.getId());
findMember.changeTeam(team);
}
}
벌크 연산 사용 O
where절에 해당하는 Entity(1개 이상)의 age값을 수정합니다.
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public int bulkOperation(int age) {
return em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
}
}
벌크 연산 문제점
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 반영합니다. 즉, DB와 영속성 컨텍스트에 저장된 Entity의 값이 서로 다름을 의미합니다.
기존에는 flush() 시점에 변경 감지를 통해 DB에 update 쿼리를 날렸습니다.
그러나 벌크 연산에서는 위의 과정을 따르지 않습니다. 일단 벌크 연산을 실행한 결과를 봅시다.
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public int bulkOperation(int age) {
return em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
}
}
@SpringBootTest
@Transactional
@Commit
class MemberJpaRepositoryTest {
@Autowired
MemberJpaRepository memberJpaRepository;
@Autowired
EntityManager em;
@Test
void queryTest() {
memberJpaRepository.save(new Member("memberA", 10, null));
memberJpaRepository.save(new Member("memberB", 10, null));
memberJpaRepository.save(new Member("memberC", 10, null));
memberJpaRepository.save(new Member("memberD", 10, null));
memberJpaRepository.save(new Member("memberE", 10, null));
int resultCount = memberJpaRepository.bulkOperation(10);
Assertions.assertThat(resultCount).isEqualTo(5);
List<Member> members = memberJpaRepository.findAll();
for (Member member : members) {
System.out.println("member = " + member);
}
}
}
애플리케이션과 DB에 저장된 Member의 age 값이 서로 다릅니다. 분명 테스트 종료시점에 Commit이 실행되면서 flush()가 호출되었을 것입니다. flush()가 호출되어야 변경 감지를 통해 DB에 update 쿼리문이 발생하는데 왜 서로 다른 값을 갖는 걸까요?
그 이유는 벌크 연산에서는 영속성 컨텍스트와 DB는 별개이기 때문입니다.
해결
벌크 연산은 영속성 컨텍스트와 DB가 별개로 작동합니다. 따라서 벌크 연산을 수행하고 영속성 컨텍스트를 초기화해야 합니다. 과거의 값이 여전히 영속성 컨텍스트에 저장되어 있기 때문에 문제가 됩니다.
SpringDataJpa에서 벌크 연산
벌크 연산을 수행하기 위해서는 @Modifying 애너테이션을 사용해야 합니다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying(clearAutomatically = true)
@Query(value = "update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkOperation(@Param("age") int age);
}
벌크 연산시 @Modifying 애너테이션을 사용하지 않으면 예외가 발생합니다.
SpringDataJpa에서도 벌크 연산시 영속성 컨텍스트와 DB가 별개로 작동합니다. 따라서 애플리케이션과 DB에 저장된 값이 서로 다르게 됩니다. 이를 해결하기 위해서 @Modifying 애너테이션에 clearAutomatically = true 속성을 추가해줍니다. 벌크 연산을 수행하고 자동으로 영속성 컨텍스트를 초기화합니다.