JPQL의 update 쿼리 호출 후 발생하는 상황에 대해 알아보자.
@Entity
public class Post {
@Id
@GeneratedValue
private Long id;
...
// Getter, Setter
}
먼저 테스트를 위해 id와 title만 있는 간단한 Post 엔티티를 생성한다.
public interface PostRepository extends JpaRepository<Post,Long> {
@Modifying
@Query("UPDATE Post p SET p.title = :title WHERE p.id = :id")
int updateTitle(String title, Long id);
}
이제 updateTitle 쿼리를 호출 하고나서 생길 수 있는 문제 상황에 대해서 알아보자.
두 가지로 상황으로 나눠본다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Rollback(false)
class PostRepositoryTest {
@Autowired
private PostRepository posts;
@Test
void updateTitle () {
Post p = createPost("hello");
posts.save(p);
String jpa = "jpa";
posts.updateTitle(jpa, p.getId());
Optional<Post> findedPost = posts.findById(p.getId());
assertThat(findedPost.get().getTitle()).isEqualTo("jpa");
}
private Post createPost(String title) {
Post p = new Post();
p.setTitle(title);
posts.save(p);
return p;
}
}
문제 상황을 보기 위해 테스트 코드를 작성했는데 테스트 코드의 순서는 다음과 같다.
- 새로운 Post 객체를 생성한다. title은 "hello" 이다.
- save()를 호출하여 생성된 Post를 저장(영속화) 한다.
- PostRepository에서 내가 만든 updateTitle()을 호출하여 "hello" -> "jpa" 로 변경하는 update 쿼리를 실행한다.
- findById()로 위의 객체를 찾는다.
- 찾은 객체가 반환한 findedPost의 title이 "jpa"로 바뀌었는지 예상한다.
테스트를 실행하면 결과는 실패한다.
테스트 결과 title이 "jpa"가 아니라 "hello"여서 실패한다.
왜 실패했을까? 쿼리와 DB를 확인해보자.
update
post
set
title=?
where
id=?
-----------------------------------
springdata=# select * from post;
id | title
----+-------
1 | jpa
콘솔을 확인하면 update 쿼리도 정상적으로 실행됐고, DB를 확인해도 "jpa"로 잘 바뀌어있다.
쿼리와 DB에는 문제가 없고 다음과 같은 문제 때문에 테스트에 실패한 것이다.
posts.save(p)
를 호출했기 때문에 이 때, 영속성 컨텍스트에 p가 저장 됐고그런데 p는 분명 "jpa"로 update 되지 않았나? 🧐🥸
update와 관련된 벌크연산 같은 쿼리는 영속성 컨텍스트를 거치지 않고 곧바로 DB로 직접 쿼리를 한다.
따라서, 영속성 컨텍스트의 p는 "hello" -> "jpa"로 바뀐줄 모르고 그저 "hello"만 알고 있는 상태이다.
1번 case의 문제를 겪고, 이런 생각이 들었다.
💡 만약 select 쿼리가 호출된다면, DB에는 데이터가 정상적으로 반영됐을테니 테스트가 성공하겠다!
잘 될 것이란 믿음을 갖고 두 번째 테스트 코드를 작성해보자.
@Test
void updateTitle () {
Post p = createPost("hi");
posts.save(p);
String jpa = "hibernate";
posts.updateTitle(jpa, p.getId());
List<Post> list = posts.findAll(Sort.by("id").descending());
assertThat(list.get(0).getTitle()).isEqualTo("hibernate");
}
case 1 코드와 유사하나 조금 다른점이 있다.
- "hi" -> "hibernate" 로 변경하는 새로운 Post 엔티티를 만들고,
- findAll()을 호출하여 무조건 select 쿼리가 발생 하도록 변경하였다.
- list의 첫 번째 데이터 title이 "hibernate" 라고 예상한다.
테스트를 실행해보면 이 코드도 실패한다. 다시 콘솔과 DB를 확인해보자.
update
post
set
title=?
where
id=?
select
post0_.id as id1_0_,
post0_.title as title2_0_
from
post post0_
order by
post0_.id desc
---------------------------------------
springdata=# select * from post order by id desc;
id | title
----+-----------
2 | hibernate
1 | jpa
DB에서 값을 select 했으면 이번에는 테스트가 성공할 것이라 생각했는데 예상과는 달리 테스트에 실패했다.
JPQL로 DB에서 조회한 엔티티가 영속성 컨텍스트에 이미 존재한다면, JPQL로 조회한 결과를 버리고 영속성 컨텍스트에 있던 기존 엔티티를 반환한다.
이러한 이유로 실패를 했는데 구체적인 그림으로 설명하면 다음과 같다.
select 쿼리로 데이터를 조회한 직후, 영속성 컨텍스트와 DB 조회 결과이다.
따라서, 내가 리턴받게된 List<Post>
에는 Post("hi")와 Post("jpa")가 리턴된다.
정리하자면 두 가지 사실을 알 수 있다.
- JPQL로 조회한 엔터티는 영속 상태다.
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.
이런 생각이 들 수 있다. DB에 반영된 데이터가 내가 원하는 최신의 데이터인데 DB에서 가져온 데이터를 왜 사용하지 않는 걸까?
이유는 영속성 컨텍스트가 영속 상태인 엔티티의 동일성을 보장해야하기 때문이다.
따라서, DB에서 조회된 결과를 버리고 기존 엔티티를 반환한다.
이러한 문제로 인해 update 쿼리 같은 영속성 컨텍스트를 무시하고 직접 DB와 쿼리하는 것은
주의해서 사용해야 한다.
상황에 따라 update 쿼리를 반드시 써야 한다면 find()를 하기전에 영속성 컨텍스트를
초기화 하면 된다 -> clear()
public interface PostRepository extends JpaRepository<Post,Long> {
@Modifying(clearAutomatically = true)
@Query("UPDATE Post p SET p.title = :title WHERE p.id = :id")
int updateTitle(String title, Long id);
}
위에서 작성한 updateTitle() 쿼리 메소드 @Modifying 애노테이션에
clearAutomatically = true 값을 주면 해당 쿼리 실행 후 영속성 컨텍스트를 clear() 해준다.
clear()로 인해 영속성 컨텍스트가 초기화(기존 엔티티 날라감) 되어서 select를 한 조회 결과가
모두 영속성 컨텍스트에 저장된다.
다시 테스트를 실행하면 두 케이스 모두 성공한다.