해당 포스팅에서는 JPA @Modifying 어노테이션을 적용할 때 clearAutomatically 속성에서 발생할 수 있는 영속성 컨텍스트의 1차 캐시 관련 문제에 대해서 살펴보겠습니다.
시작에 앞서 JPA와 Spring Data JPA에 대해서 간단하게 이야기하면,
'JPA'는 Java Persistence API의 약자로, 자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스입니다.
그리고 'Spring Data JPA'는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 만들어진 모듈 중 하나인데, JPA를 한 단계 추상화시킨 Repository라는 인터페이스를 제공함으로써 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 할 수 있도록 지원합니다.
하지만 편리함을 목적으로 만들어진 모듈인 Spring Data JPA를 사용하더라도 @Query 어노테이션을 통해 직접 쿼리를 작성해야 하는 경우가 있는데요. 이어지는 내용을 통해 살펴보겠습니다.
@Query
Spring Data JPA의 쿼리 메서드보다 좀 더 구체적인 조건 등을 지정하기 위해서 @Query 어노테이션을 사용하게 됩니다.
이때 쿼리는 SQL과 유사한 JPQL(Java Persistence Query Languate)라는 JPA에서 사용하는 객체지향 쿼리 문법을 이용하거나, @Query 속성의 value 값으로 SQL문을 지정하고, nativeQuery 속성 값을 true로 지정하여 일반적으로 Database에 사용하는 SQL문을 사용할 수도 있습니다.
(JPQL을 사용하는 경우 JPA의 구현체에서 이를 해석하고 실행하게 됩니다.)
public interface exampleRepository extends JpaRepository<Example, Long> {
@Query(value = "select example_id, example_name from example", nativeQuery = true)
public List<Example> selectAll();
}
@Modifying
@Modifying 어노테이션은 @Query 어노테이션으로 작성된 INSERT, UPDATE, DELETE 쿼리를 사용할 때 필요하며(SELECT 제외), 주로 다중 UPDATE 또는 DELETE 같은 복잡한 벌크 연산을 하나의 쿼리로 수행할 때 사용합니다.
***
INSERT, UPDATE, DELETE 쿼리에서 @Modifying 어노테이션을 사용하지 않는 경우에는 아래와 같은 오류가 발생하게 됩니다.
org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations [update ...]
flushAutomatically(), clearAutomatically()
@Modifying 어노테이션을 적용하게 되면 JPA Entity Life Cycle을 무시하고 쿼리가 실행되기 때문에 영속성 컨텍스트 관리에 주의해야 하는데요. 영속성 컨텍스트는 아래 두 가지 속성을 통해 관리할 수 있습니다.
@Modifying 어노테이션에는 flushAutomatically, clearAutomatically 두 가지 속성이 있으며, @Modifying 어노테이션만 적용하였을 때, 두 가지 속성의 default 값은 모두 false입니다.
flushAutomatically
해당 속성은 @Query와 @Modifying을 통한 쿼리 메서드를 사용할 때, 해당 쿼리를 실행하기 전, 영속성 컨텍스트의 변경 사항을 Database에 flush 할 것인지를 결정하는 속성입니다. default 값은 false입니다.
clearAutomatically
해당 속성은 @Modifying이 적용된 쿼리 메서드를 실행한 후, 영속성 컨텍스트를 clear 할 것인지를 지정하는 속성이며, default 값은 flasuAutomatically 속성과 마찬가지로 false입니다.
이때 default 값인 false를 그대로 사용할 경우, 영속성 컨텍스트의 1차 캐시와 관련한 문제점이 발생할 수 있는데, 이어지는 예시를 통해 발생할 수 있는 문제점과 해결 방안을 살펴보겠습니다.
clearAutomatically 속성 값이 false일 때, 발생할 수 있는 문제
앞에서 이야기한 것처럼 @Modifying 어노테이션에는 clearAutomatically, flushAutomatically 두 가지 속성이 있으며, 두 속성 모두 default 값은 false인데요.
여기서 clearAutomatically 속성이 default 값인 false인 경우 발생할 수 있는 영속성 컨텍스트의 1차 캐시 관련 문제에 대해서 살펴보겠습니다.
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long score = 0L;
}
(예시를 위한 Member Entity입니다.)
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying
@Query("update Member m set m.score = :score where m.id = :id")
void updateScore(Long id, Long score);
}
(MemberRepository입니다. @Modifying 어노테이션의 clearAutomatically 속성은 default 값인 false입니다.)
@Transactional
public void clearAutomaticallyTest() {
Member member = new Member(); // Transient 상태
memberRepository.save(member); // Persistent 상태
System.out.println("============= before score : " + member.getScore());
memberRepository.updateScore(1L, 100L);
System.out.println("============= after score : " + memberRepository.findById(1L).get().getScore());
}
(테스트 로직)
clearAutomatically 속성 값 false 일 경우의 실행 결과입니다.
update 문이 실행되었지만 findById(1L)로 가져온 Member의 score 값이 0으로 나오는 것을 볼 수 있는데요.
반면 데이터베이스에 저장된 score 값은 100으로 만약 score 값을 사용해서 이어지는 로직이 있었을 경우 잘못된 결과가 나올 수 있다는 문제점이 확인되었습니다.
이때 주목해야 하는 부분이 바로 'Persistent' 즉, 영속 상태입니다.
쉽게 객체를 저장한 상태라고 이해할 수 있는데, save() 메서드를 통해 ID를 할당하며, 이후 트랜잭션이 끝날 때까지 모든 변경 사항을 감지하고 동기화하는 상태입니다.
(중요) Persistent 상태에서는 하이버네이트가 한 트랜잭션 내에서 불필요한 쿼리를 줄여주는 중요한 역할을 하는데요. 흔히 1차 캐시라고 부르는 영속성 컨텍스트(Persistent Context)가 해당 인스턴스를 이미 담고 있기 때문에 SELECT 요청을 하더라도 DB에 접근하지 않고, Persisent Context에 저장된 인스턴스를 반환하게 됩니다.
결론적으로 @Query와 @Modifying을 사용한 벌크 연산에서는 1차 캐시를 포함한 영속성 컨텍스트를 무시하고 바로 쿼리를 실행하기 때문에 영속성 컨텍스트는 데이터 변경을 알 수 없습니다. 즉 1차 캐시(Persistent ConText)와 DB에 데이터가 일치하지 않을 수 있습니다.
그렇다면 clearAutomatically = true 일 때는?
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying(clearAutomatically = true)
@Query("update Member m set m.score = :score where m.id = :id")
void updateScore(Long id, Long score);
}
(다른 코드는 그대로 두고 clearAutomatically 속성 값만 true로 변경)
clearAutomatically 속성 값 true 일 경우의 실행 결과입니다.
결과 score 값이 100으로 데이터베이스의 값과 일치하게 나왔습니다. 이때 먼저 테스트한 속성 값이 false일 때와의 차이점은 update 문 이후 select 문이 한번 더 날아갔다는 점인데요.
clearAutomatically = true 즉, @Modifying 어노테이션이 적용된 쿼리 메서드를 실행한 후, 영속성 컨텍스트(Persistent Context)에 담겨있는 인스턴스를 clear 했다는 것입니다.
때문에 findById(1L) 부분에서 1차 캐시에 저장된 인스턴스가 없기 때문에 DB에 SELECT 요청을 하여 DB에 있는 score 값을 가지고 온 것입니다.
< 참고 자료 >
'Programming > Spring Boot' 카테고리의 다른 글
@RequestBody @ResponseBody 어노테이션 이해하고 사용하기 (2) | 2022.05.13 |
---|---|
@Value 어노테이션 null이 나오는 문제 해결 방법 (5) | 2022.05.07 |
생성자 주입과 필드 주입, 수정자 주입 정리 (feat. 의존성 관계 주입) (0) | 2022.03.31 |
Spring Batch 개념과 구조, 동작 방식에 대한 정리 (0) | 2022.03.15 |
Redis 동시성 처리를 위한 Transaction 사용 (MULTI, EXEC, DISCARD, WATCH) (2) | 2022.03.05 |