📌 @Modifying?
Spring Data JPA를 사용하는 경우, @Query를 통해 JPQL을 작성하는 상황이 있습니다. 예시로, 상품의 모든 가격을 인상해야 하는 상황이라고 가정해 보겠습니다. 이때 변경 감지를 통해서 정보를 업데이트한다면 상품 개수별로 쿼리가 각각 실행됩니다. 주로 이러한 상황에 우리는 @Query를 통해 벌크 연산을 수행하게 됩니다.
@Query("update Item i set i.price = i.price + 1000")
@Modifying
void increasePrice();
@Query는 네임드 쿼리(Named Query)를 인터페이스에서 바로 사용할 수 있게 하는 어노테이션입니다.
이때 @Query로 작성된 JPQL이 DML(INSERT, DELETE, UPDATE 쿼리)을 사용하는 경우 @Modifying을 사용합니다. 만약 사용하지 않는다면 Hibernate는 다음 Exception을 발생시킵니다.
org.hibernate.hql.internal.QueryExecutionRequestException: Not supported
for DML operations
저는 처음에 @Query를 통해 데이터를 수정할 때 이러한 오류를 보고 @Modifying을 추가하여 해결하였습니다. 그런데 왜 이 어노테이션을 추가해야할까요?
📌 순수 JPA에서의 수정
Spring이 추상화하여 제공하는 data JPA 대신 순수한 JPA를 들여다봅시다.
JPA에서 이러한 벌크 연산은 어떻게 수행할까요?
// 데이터 삽입
Item item1 = new Item();
item1.setPrice(1000);
em.persist(item1);
Item item2 = new Item();
item2.setPrice(1000);
em.persist(item2);
Item item3 = new Item();
item3.setPrice(1000);
em.persist(item3);
// 의도한 벌크 연산
em.createQuery("update Item i set i.price = i.price + 1000")
.executeUpdate();
List<Item> result = em.createQuery("select i from Item i", Item.class)
.getResultList();
for (Item item : result) {
System.out.println("item.getPrice() = " + item.getPrice());
}
이 코드를 수행한 후 DB 내 ITEM은 모두 2000원으로 수정되었습니다.
그러면 코드의 출력값은 어떨까요?
item.getPrice() = 1000
item.getPrice() = 1000
item.getPrice() = 1000
DB에는 의도한 수정 결과가 제대로 반영되었는데, 코드의 ITEM에는 수정값이 반영되지 않은 1000원이 그대로 있습니다.
JPA의 동작 구조
우리가 em.createQuery("update Item i set i.price = i.price + 1000").executeUpdate()를 수행하기 전 영속성 컨텍스트 FLUSH가 이루어집니다. 따라서 item1, item2, item3의 INSERT 쿼리가 실행됩니다.
그다음 UPDATE 쿼리가 실행되어 item1, item2, item3의 가격이 2000원으로 수정됩니다. 하지만 영속성 컨텍스트에는 반영되지 않아 기존에 있던 item1, item2, item3를 가져옵니다. 따라서 영속성 컨텍스트를 초기화한 후 다시 조회하지 않는 이상, item의 가격은 1000원이 나오게 됩니다.
UPDATE 쿼리가 실행하고 나서 em.clear()를 수행한 후 조회한다면 Item의 가격은 2000원으로 조회하게 됩니다.
즉, UPDATE 쿼리를 수행한 후에는 업데이트된 내용이 영속성 컨텍스트에 반영되지 않으므로 영속성 컨텍스트는 outdated한 상태가 됩니다.
📌 Data JPA의 @Modifying
그럼 JPA를 추상화하여 사용할 수 있게 해주는 Data JPA에서 @Modifying은 무슨 역할일까요?
단순히 옵션 없이 @Modifying만 쓰는 경우라면 이를 DML을 사용하는 쿼리라고 명시하는 역할입니다.
하지만 우리가 예시에서 영속성 컨텍스트를 초기화하여 변경된 값을 가져왔듯이, @Modifying에 옵션을 설정하여 수정 후 영속성 컨텍스트를 초기화할 수 있습니다.
@Modifying(clearAutomatically = true)
이 옵션을 통해 쿼리를 수행한 후, 영속성 컨텍스트를 초기화를 시킬 수 있습니다.
그런데 살펴보면 다른 옵션도 존재하는 것을 알 수 있습니다.
@Modifying(flushAutomatically = true)
이 옵션은 쿼리 실행 전 FLUSH되지 않은 정보가 있다면 이를 DB에 반영하기 위해 실행 전 FLUSH를 하게 하는 옵션입니다.
그런데 위에서 볼 때 em.createQuery(…)를 실행했을 때 알아서 FLUSH가 진행되어 item1, item2, item3이 저장되었습니다. 따로 옵션을 적용하지 않았는데 어떻게 된 걸까요?
그 이유는 기본으로 FlushModeType.AUTO로 설정되어 있기 때문입니다. FlushModeType.AUTO는 디폴트 설정으로, JPQL이나 네이티브 쿼리가 실행되기 전 FLUSH를 수행합니다.
따라서 이 설정이 적용되어 있지 않은 경우, @Modifying(flushAutomatically=true)은 FLUSH를 강제합니다.
'스프링' 카테고리의 다른 글
알림을 비동기 방식으로 보내기 (0) | 2025.02.25 |
---|---|
[JPA] 연관관계의 주인이란? (0) | 2025.01.13 |
Mybatis vs JPA (0) | 2025.01.08 |
SELECT 작업에 트랜잭션은 필요할까? (0) | 2025.01.08 |
스프링 트랜잭션 (0) | 2025.01.07 |