인프런 김영한 강사님 강의를 듣고 정리한 내용입니다.
스프링 데이터 JPA에서 일반적으로 update와 관련해서는 쿼리를 따로 만들지 않는다.
그 이유는 update가 트랜잭션 과정에서 객체의 값을 변경
하는 것으로 이루어지기 때문이다. (setter를 열어두었다면 setter로 값을 변경하고, 그렇지 않다면 update를 위한 메서드를 엔티티 내부에 두어 이를 사용해 값을 변경한다.) 그리고 트랜잭션이 끝날 때 Dirty checking을 통해 변경된 값을 확인하고, update 쿼리를 자동으로 수행해 DB에 반영된다.
하지만 여러 튜플을 하나의 쿼리로 한 번에 update 하려는 경우도 있을 것이다. 이와 같은 쿼리를 벌크성 쿼리
라고 부르며, 벌크성 쿼리를 리포지토리 내에 정의할 수 있다.
첫문단에서 설명했던 이유 때문에 벌크성 쿼리는 @Modifying
어노테이션이 필수이다. 포함하지 않으면 executeQuery()
가 아닌 getResult()
를 호출하게 된다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
}
위와 같이 쿼리하면 age 이상의 모든 member의 나이를 1살 씩 늘려주게 된다.
트랜잭션 내에서 엔티티들은 영속성 컨텍스트
에 의해 관리된다. 트랜잭션 시작 시에 영속성 컨텍스트가 초기화 된다. 그리고 트랜잭션 내에서 단건 업데이트 쿼리
가 이루어지면 영속성 컨텍스트의 1차 캐시에 업데이트가 이루어진다.(1차 캐시는 트랜잭션 내에서만 사용하는 캐시를 말함.) 그래서 다시 조회할 때 DB에 직접 접근하여 값을 가져오는 대신, 영속성 컨텍스트의 1차 캐시 값을 읽게 된다. 그리고 트랜잭션이 종료될 때 영속성 컨텍스트 내의 1차 캐시 값과 DB의 값을 비교하는 Dirty Checking
을 통해서 DB에 반영이 된다.
하지만 벌크성 쿼리를 하게 되면 영속성 컨텍스트와 무관하게 DB에 바로 update 가 반영된다. 영속성 컨텍스트는 update에 대해서 감지하지 못하게 된다. 벌크성 쿼리 후 다시 조회를 했을 때 영속성 컨텍스트의 1차 캐시 값을 읽게 되고, update에 대해서 알지 못하는 영속성 컨텍스트는 update 이전 값을 반환하게 된다.
결론적으로 벌크성 쿼리 이후에 다른 로직을 실행하는 경우 오류가 발생할 수 있다는 문제가 있다.
해결방법은 벌크성 쿼리를 한 직후 영속성 컨텍스트를 초기화하는 것이다.
EntityManager를 주입받아서 초기화 해주어도 된다.(flush(), clear())
하지만 스프링 데이터 JPA는 이와 같은 문제를 대비하여 영속성 컨텍스트 초기화를 위한 기능을 지원한다.
위의 코드를 다음과 같이 바꿔주면 된다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
}
@Modifying(clearAutomatically = true)
로 바꿔주면 된다. default는 false이지만, true로 설정하면 이 쿼리를 수행한 직후에 영속성 컨텍스트를 자동으로 초기화해준다.
두번째 해결방법으로 벌크성 쿼리를 한 후에 다른 로직을 수행하지 않고 바로 트랜잭션을 종료하는 방법이 있다. 이 경우에는 영속성 컨텍스트의 초기화가 필요 없다.