개발

@Modifying 의 flushAutomatically 속성을 명시해야 하는 이유

모달조아 2023. 4. 9. 00:17

개요

@Modifying 어노테이션을 사용하다가 생긴 이슈를 해결하며 얻은 지식을 작성해보고자 합니다.

일단 먼저 정답부터 이야기하자면, hibernate 의 FlushModeType 설정의 기본 값인 Auto 일 때, JPQL 실행 전에 모든 쓰기 지연 저장소의 쿼리들이 flush 되는 줄로 잘못 알고 있어서 발생한 이슈입니다.

JPQL 실행 전에  쓰기 지연 저장소에 있는 쿼리 중 JPQL 과 관련된 엔티티에 대해서만 flush 합니다. 더 자세한 설명은 아래에서 이어서 하도록 하겠습니다.

 

@Modifying 이란

@Modifying 이 무엇인지부터 먼저 알아보겠습니다. @Modifying 어노테이션은 @Query를 이용하여 INSERT, UPDATE, DELETE쿼리를 작성할 경우 사용해줘야하는 어노테이션입니다. 사용하지 않으면 QueryExecutionRequestException 이 발생합니다.

@Modifying 어노테이션에 사용가능한 속성은 두 가지가 있습니다.

  • clearAutomatically: 쿼리 실행 후 영속성 컨텍스트를 비우도록 만듬
  • flushAutomatically: 쿼리 실행 전 쓰기 지연 저장소의 쿼리들을 flush 하도록 만듬

 

이슈

@Modifying
@Query("UPDATE Image i SET i.deletedAt = NOW() "
		+ "WHERE i.deletedAt IS NULL AND i.referenceId = :referenceId AND i.imageType = :imageType")
void softDeleteByReferenceIdAndImageType(
		@Param("referenceId") Long referenceId,
		@Param("imageType") ImageType imageType
);

이미지의 soft delete 를 위해 위의 메서드를 사용하고있었습니다.

위 메서드는 JPQL 을 이용하였으므로 영속성 컨텍스트를 거치지 않고 DB 에 바로 쿼리를 날립니다. 그렇기에 쿼리 실행 후 DB 와 영속성 컨텍스트는 동기화 되지 않은 상태입니다. 만약 이 상황에서 조회를 한다면, 영속성 컨텍스트를 기반으로 하기에 잘못된 정보가 조회될 것입니다.

그래서 아래와 같이 @Modifying 의 속성 clearAutomatically 를 true 로 하여 쿼리 커밋 후 영속성 컨텍스트를 비워주는 방식으로 동기화가 안되는 문제를 해결하였습니다. flushAutomatically 를 명시해주지 않은 이유는 hibernate 의 FlushModeType 의 기본 값 auto 가 JPQL 실행 전 flush 를 한다고 생각하여 따로 명시해주지 않았습니다.

@Modifying(clearAutomatically = true)
@Query("UPDATE Image i SET i.deletedAt = NOW() "
		+ "WHERE i.deletedAt IS NULL AND i.referenceId = :referenceId AND i.imageType = :imageType")
void softDeleteByReferenceIdAndImageType(
		@Param("referenceId") Long referenceId,
		@Param("imageType") ImageType imageType
);

그런데, 기존에는 통과하던 테스트가 clearAutomatically 를 true 로 바꿔주고부터 실패했습니다. 실패한 테스트 코드를 살펴보겠습니다.

@Test
@DisplayName("Fail - Order 삭제 실패.(Conflict)")
void deleteOrderConflict() throws Exception {
	//given
	Member member = memberRepository.save(getMember("member"));
	setContext(member.getId(), USER);
			
	Order order = orderRepository.save(getOrder(member.getId()));
	order.upDateOrderStatus(OrderStatus.RESERVED);

	//when //then
	mockMvc.perform(delete("/api/v1/orders/{orderId}", order.getId())
					.header("access_token", ACCESS_TOKEN)
					.with(csrf())
			).andExpect(status().isConflict())
			.andDo(print())
			.andDo(document(
					"order/주문 삭제 실패(Conflict)",
					getDocumentRequest(),
					getDocumentResponse(),
					requestHeaders(
							headerWithName("access_token").description("인가 토큰")
					),
					pathParameters(
							parameterWithName("orderId").description("주문 식별자")
					)
			));
}

간단하게 설명을 하자면, Order 가 이미 예약된 상태면 삭제하지 못하는 것이 비즈니스 요구사항입니다. 그 요구사항을 만족하는지를 테스트하기 위해 Order 의 상태를 예약됨 상태로 변경 후, 삭제하였을 때 409 에러가 발생하는지 확인하는 로직입니다.

409 를 기대하지만 실제로 나오는 결과는 204 입니다. 즉, 데이터가 정상적으로 삭제가 되었다는 의미입니다.

Order order = orderRepository.save(getOrder(member.getId()));
order.upDateOrderStatus(OrderStatus.RESERVED);

일단 추측하기로는 이 upDateOrderStatus 메서드가 반영되지 않은 것입니다. 현재는 변경 감지 방식으로 upDateOrderStatus 에 해당하는 쿼리가 나가는 상태입니다.

이 단계에서 제가 생각했던 실행의 순서는 아래와 같습니다.

  1. orderRepository.save(getOrder(member.getId())); 로 Order 를 영속화
  2. order.upDateOrderStatus(OrderStatus.RESERVED); 로 1차 캐시의 Order 변경
  3. mockMvc 를 통한 Order delete api 를 호출하고 그 과정에서 Order 의 이미지 삭제 JPQL 호출
    • JPQL 쿼리 실행 전 Order 의 변경 감지 후 쓰기 지연 저장소로 보낸 후 flush
  4. JPQL 반영
  5. clearAutomatically  = true 로 인한 영속성 컨텍스트 비움

 

clearAutomatically 를 true 로 바꾸기 전에는 변경 감지로 생긴 쿼리가 잘 반영되었는데 clearAutomatically 를 true 로 바꾸어서, JPQL 실행 후 영속성 컨텍스트를 비우게하니 변경 감지로 생긴 쿼리가 반영되지 않았다. 저는 3번 단계에서 flush 가 일어나지 않아서 update 쿼리가 쓰기 지연 저장소에 남아있게 되었고, 5번 단계에서 남아 있는 쿼리가 사라져서 발생한 이슈라고 추측했습니다.

추측을 확인해보기 위해서 아래와 같이 flushAutomatically 속성도 true 로 해주고 다시 테스트를 돌려보겠습니다.

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE Image i SET i.deletedAt = NOW() "
		+ "WHERE i.deletedAt IS NULL AND i.referenceId = :referenceId AND i.imageType = :imageType")
void softDeleteByReferenceIdAndImageType(
		@Param("referenceId") Long referenceId,
		@Param("imageType") ImageType imageType
);

테스트가 성공합니다.

그렇다면, JPQL 실행 전에 flush 가 되지 않았다는 것인데 왜 flush 가 되지 않았을까요?

hibernate 의 FlushModeType 의 기본 값은 auto 이고, 이때 트랜잭션 내에서 JPQL 이 실행되면 영속성 컨텍스트가 flush 된다고 알고 있었기에 굉장히 의문이었습니다.

hibernate 공식 문서에서 답을 얻을 수 있었습니다.

2번째 리스트를 해석해보자면, 대기 중인 엔티티 작업과 겹치는 JPQL 쿼리를 실행하기 전에 flush 가 동작합니다.

즉, JPQL 에 의해 영향 받는 엔티티의 쿼리들만 flush 된다는 의미입니다.

 

이제 아까 clearAutomatically 속성만 true 로 줬을 때, 왜 테스트가 실패했는지 이유를 알았습니다.

mockMvc 를 통해서 Order 의 delete api 가 호출될 때 실행되는 JPQL 은 이미지와 관련된 것이고, 기존에 쓰기 지연 저장소에 있던 쿼리는 Order 의 상태를 변경시키는 쿼리이니 둘 사이에는 관계가 없습니다. 그렇기 때문에 JPQL 실행 전에 Order 변경 쿼리가 flush 되지 않았고, JPQL 이 실행되고 난 후 clearAutomatically = true 속성으로 인해 영속성 컨텍스트가 싹 비워지기에 Order 변경 쿼리는 사라집니다. 그러므로, order 상태가 예약됨 상태로 변경되지 않았기에 기대했던 409 응답이 발생하지 않았던 것입니다.