본문 바로가기

JPA

Spring Data JPA @Modifying (1) - clearAutomatically

이 글을 작성하게 된 계기는 Spring Data JPA의 @Modifying에 있는 flushAutomatically에 대해 의문점이 생겼고, 그에 대한 학습 테스트를 해보면서 였습니다. 하지만 @Modifying의 Attribute가 clearAutomatically, flushAutomatically 두 개 뿐이여서 같이 정리하고자 해서 작성하게 되었습니다.

 

 

@Query

@Modifying을 말씀드리기 전에 먼저 @Modifying이 적용되는 @Query에 대해 알고 넘어갑시다.

Spring Data JPA에서는 기본적으로 JpaRepository를 통해서 제공되는 findById와 같은 메서드도 있고, 메서드 네이밍만을 통해서 쿼리를 실행할 수 있도록 기능을 제공해주고 있습니다.

하지만, 이 두가지 방법으로도 만들 수 없는 쿼리가 필요하다면, 쿼리를 직접 작성해야 합니다. 그 때 커스텀 Reopository의 메서드에 붙이는 annotation이 @Query입니다.

기본적으로는 JPQL로 작성할 수 있고, nativeQuery=true 옵션으로 네이티브 쿼리도 사용 가능합니다.

 

 

@Modifying

https://docs.spring.io/spring-data/data-jpa/docs/current/api/org/springframework/data/jpa/repository/Modifying.html

 

Modifying (Spring Data JPA 2.2.6.RELEASE API)

Indicates a query method should be considered as modifying query as that changes the way it needs to be executed. This annotation is only considered if used on query methods defined through a Query annotation). It's not applied on custom implementation met

docs.spring.io

이 Annotation은 @Query Annotation으로 작성 된 변경, 삭제 쿼리 메서드를 사용할 때 필요합니다. 즉, 조회 쿼리를 제외하고, 데이터에 변경이 일어나는 INSERT, UPDATE, DELETE, DDL 에서 사용합니다. 주로 벌크 연산 시에 사용됩니다.

(기본적으로 제공되는 쿼리 메서드나 메서드 네이밍으로 파생되는 쿼리 메서드에서는 적용이 되지 않습니다.)

 

JPA Entity LifeCycle을 무시하고 쿼리가 실행되기 때문에 해당 annotation을 사용할 때는 영속성 콘텍스트 관리에 주의해야 합니다. 앞으로 설명할 clearAutomatically, flushAutomatically를 통해 간단하게 해결할 수 있습니다.

 

 

벌크 연산

벌크 연산이란 단건 UPDATE, DELETE 제외한 다건의 UPDATE, DELETE 연산을 하나의 쿼리 하는 것을 의미합니다. JPA에서 단건 UPDATE 같은 경우에는 Dirty Checking  통해서 수행되거나 save()로도 가능합니다. DELETE 같은 경우에는 다건, 단건 모두 쿼리 메서드로 제공됩니다. 여기서는 @Modifying @Query 사용한 벌크 연산 살펴보겠습니다.

이 방법의 장점은 직접 자유롭게 JQPL를 정의해서 사용할 수 있고, 하나의 쿼리로 많은 데이터를 변경할 수 있다는 점입니다.

 

@Query에 벌크 연산 쿼리를 작성하고, @Modifying을 붙이지 않으면, InvalidDataAccessApiUsage exception이 발생합니다.

https://www.baeldung.com/spring-data-jpa-modifying-annotation

 

 

 

clearAutomatically

이 Attribute는 @Modifying 이 붙은 해당 쿼리 메서드 실행 직 후, 역속성 컨텍스트를 clear 할 것인지를 지정하는 Attribute 입니다. default 값은 false 입니다. 하지만 default 값인 false를 그대로 사용할 경우, 영속성 컨텍스트의 1차 캐시와 관련한 문제점이 발생할 수 있습니다.

 

 

문제점

JPA 에서는 영속성 컨텍스트에 있는 1차 캐시를 통해 엔티티를 캐싱하고, DB의 접근 횟수를 줄임으로써 성능 개선을 합니다. 1차 캐시는 @Id값을 key 값으로 엔티티를 관리합니다. 그래서 findById 등을 통해 엔티티를 조회 했을 시 @Id값이 1차 캐시에 존재한다면 DB에 접근하지 않고, 캐싱된 엔티티를 반환합니다.

그렇다면, 벌크 연산을 통해 데이터 변경 쿼리를 실행하고, 해당 엔티티를 조회하면 어떤 일이 발생할까요?? 예측해봅시다. 테스트 시나리오는 다음과 같습니다.

 

   1. 엔티티 객체를 하나 생성합니다. (해당 엔티티는 transient 상태)

   2. 객체의 초기값을 설정합니다.

   3. 해당 엔티티의 Repository를 통해 해당 엔티티 객체를 save() 합니다.  (해당 엔티티는 persistent 상태)

   4. 벌크 연산을 통해 기존에 설정한 초기값을 변경합니다. (SQL 실행)

         (이 때, flush가 발생하여 해당 쿼리 메서드가 실행 되기 전의 쿼리들이 모두 flush 됩니다. - 이 부분은 flushAutomatically에서 설명하겠습니다.)

   5. findById()를 통해 해당 엔티티를 조회합니다. 

 

 

 

어떤 결과가 나왔을 까요?? 예제를 통해 살펴봅시다.

 

 

 

예제1) @Modifying

Article

간단한 Article Entity 를 만들었습니다.

 

 

 

ArticleRepository

ArticleRepository를 만들었습니다. title을 UPDATE하는 쿼리 메서드를 정의했습니다.

@Modifying 과 @Query 를 통해 직접 JPQL을 정의했습니다. clearAutomatically는 default로 false 입니다.

 

 

 

학습 테스트

학습 테스트를 작성하였습니다. 시나리오는 다음과 같습니다.

1. Article 엔티티 생성
2. title 값 세팅
3. articleRepository를 통해 영속화
4. 벌크 연산 쿼리 메서드를 통해 title 값 UPDATE 
5. findById()를 통해 기존의 Id값으로 엔티티 조회

 

참고1) @DataJpaTest를 사용하면 자동으로 embedded DB로 대체되어서 실제 DB를 확인할 수 없게 됩니다. AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)은 embedded DB가 아닌 properties에 정의한 DataSource를 사용할 수 있게 합니다.

 

참고2) @DataJpaTest는 기본적으로 @Transactional을 포함하고 있습니다. 그래서 각 테스트 메서드를 실행할 때, 데이터를 RollBack 시키도록 되어 있습니다. 데이터가 RollBack이 된다면 실제 DB에서 Table을 확인할 수 없으므로 @RollBack(false)를 이용해 테스트 메서드가 끝나도 RollBack이 되지 않게 하였습니다. 또한 해당 코드에서는 벌크 연산 실행 시 flush를 하기 때문에 이전의 모든 쿼리가 실행되어 볼 수 있지만, Hibernate가 @DataJpaTest의 @Transactional의 RollBack을 인지하고, RollBack할 쿼리는 실행하지 않는 문제가 발생할 수 있습니다.

 

 

결과 화면

결과 화면입니다. 결과는 변경된 값, "after"가 아닌 변경 전의 값 "before"가 나왔습니다. 다들 예상하셨나요??

그렇다면 DataBase에는 값이 잘 변경되었는지 확인해보겠습니다.

 

 

 

H2 DataBase

실제 DB 조회 화면입니다. 실제 DB에서는 벌크 연산의 실행 결과로 title이 "after"로 변경된 것을 확인할 수 있습니다.

 

 

 

 

 

 

예제2) @Modifying(clearAutomatically = true)

ArticleRepository

@Modifying(clearAutomatically = true)를 설정해 주었습니다.

이 외의 나머지 코드들은 완전히 동일하기 때문에 생략하겠습니다.

 

 

 

결과 화면

결과 화면을 보시면 예제1 에 비해 2가지 차이점이 있습니다.

첫번째는 저희가 관심있게 보았던 title 값입니다. 벌크 연산에서 실행했듯이 "after"로 변경된 것을 확인할 수 있습니다.

두번째는 SELECT Query 입니다. 예제 1과 예제2 모두 테스트 코드(findById)는 동일합니다. 그런데 예제1 에서는 SELECT Query 가 실행되지 않았고, 예제2 에서는 SELECT Query 가 실행되었습니다.

 

 

 

H2 DataBase

DB에도 예제 1과 동일하게 변경 된 값인 "after"가 들어가 있는 것을 확인할 수 있습니다.

 

결론

JPA의 1차 캐시는 DB에 접근 횟수를 줄이는 성능 개선을 할 수 있는 좋은 기능이지만, @Modifying@Query 를 사용한 벌크 연산에서는 이 기능을 때문에 예측하지 못한 결과가 나올 수 있습니다. 그 원인은 다음과 같습니다.

 

JPA 에서 조회를 실행할 시에 1차 캐시를 확인해서 해당 엔티티가 1차 캐시에 존재한다면 DB에 접근하지 않고, 1차 캐시에 있는 엔티티를 반환합니다.

하지만 벌크 연산은 1차 캐시를 포함한 영속성 컨텍스트를 무시하고 바로 Query를 실행하기 때문에 영속성 컨텍스트는 데이터 변경을 알 수가 없습니다.

즉, 벌크 연산 실행 시, 1차 캐시(영속성 컨텍스트)와 DB의 데이터 싱크가 맞지 않게 되는 것입니다.

 

하지만, Spring Data JPA는 @Modifying의 clearAutomatically 유용한 기능을 제공하기 때문에 이를 쉽게 해결할 수 있습니다. clearAutomatically를 true로 변경해준다면, 벌크 연산 직 후 자동으로 영속성 컨텍스트를 clear 해줍니다.

그래서 조회를 실행하면 1차캐시에 해당 엔티티가 존재하지 않기 때문에 DB 조회 쿼리를 실행하게 됩니다.

이렇게 하면, 데이터 동기화 문제를 해결할 수 있습니다.

 

 

 

 

 

 

 

예제 코드

https://github.com/men7627/devhyogeoncodes/tree/master/clearautomatically