[JPA] JPQL update 쿼리(벌크)와 영속성 컨텍스트

janjanee·2021년 2월 26일
9

JPA

목록 보기
1/1

JPQL의 update 쿼리 호출 후 발생하는 상황에 대해 알아보자.

Post Entity

@Entity
public class Post {
    
    @Id
    @GeneratedValue
    private Long id;

    ...
		
		// Getter, Setter
}

먼저 테스트를 위해 id와 title만 있는 간단한 Post 엔티티를 생성한다.

PostRepository

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);

}
  1. PostRepository를 인터페이스 만들고 JpaRepository 인터페이스(Spring Data JPA)를 상속받는다.
  2. updateTitle 쿼리를 선언한다. 매개변수로 전달받은 값으로 title을 변경하는 간단한
    쿼리를 작성한다.

이제 updateTitle 쿼리를 호출 하고나서 생길 수 있는 문제 상황에 대해서 알아보자.
두 가지로 상황으로 나눠본다.

case 1)

@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;
    }

}

문제 상황을 보기 위해 테스트 코드를 작성했는데 테스트 코드의 순서는 다음과 같다.

  1. 새로운 Post 객체를 생성한다. title은 "hello" 이다.
  2. save()를 호출하여 생성된 Post를 저장(영속화) 한다.
  3. PostRepository에서 내가 만든 updateTitle()을 호출하여 "hello" -> "jpa" 로 변경하는 update 쿼리를 실행한다.
  4. findById()로 위의 객체를 찾는다.
  5. 찾은 객체가 반환한 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에는 문제가 없고 다음과 같은 문제 때문에 테스트에 실패한 것이다.

  1. findById()를 호출 했을 때, select 쿼리는 발생하지 않았다.
  2. findById()는 DB를 확인하기 전에 우선적으로 영속성 컨텍스트(1차 캐시)에 찾으려는 Id의 엔티티가 존재하는지를 확인한다.
  3. findById() 호출전에 posts.save(p) 를 호출했기 때문에 이 때, 영속성 컨텍스트에 p가 저장 됐고
  4. 따라서, 영속성 컨텍스트에 있는 p를 그대로 반환하는데, 이 때의 p의 title 값은 "hello"이다.

그런데 p는 분명 "jpa"로 update 되지 않았나? 🧐🥸

update와 관련된 벌크연산 같은 쿼리는 영속성 컨텍스트를 거치지 않고 곧바로 DB로 직접 쿼리를 한다.
따라서, 영속성 컨텍스트의 p는 "hello" -> "jpa"로 바뀐줄 모르고 그저 "hello"만 알고 있는 상태이다.


case 2)

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
  • update -> select 까지 쿼리는 문제가 없다. 예상하던 select 쿼리가 호출이 됐다.
  • DB를 확인하면 "hi" -> "hibernate" 로 잘 바뀌어있다.

DB에서 값을 select 했으면 이번에는 테스트가 성공할 것이라 생각했는데 예상과는 달리 테스트에 실패했다.

JPQL로 DB에서 조회한 엔티티가 영속성 컨텍스트에 이미 존재한다면, JPQL로 조회한 결과를 버리고 영속성 컨텍스트에 있던 기존 엔티티를 반환한다.

이러한 이유로 실패를 했는데 구체적인 그림으로 설명하면 다음과 같다.


select 쿼리로 데이터를 조회한 직후, 영속성 컨텍스트와 DB 조회 결과이다.

  1. @Id (1)은 영속성 컨텍스트에 없기 때문에 DB 조회한 결과가 영속성 컨텍스트에 저장된다.
  2. @Id (2)는 영속성 컨텍스트에 이미 존재하므로 DB 조회 결과가 버려지고 기존 엔티티를 반환한다.

따라서, 내가 리턴받게된 List<Post>에는 Post("hi")와 Post("jpa")가 리턴된다.

정리하자면 두 가지 사실을 알 수 있다.

  • JPQL로 조회한 엔터티는 영속 상태다.
  • 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.

이런 생각이 들 수 있다. DB에 반영된 데이터가 내가 원하는 최신의 데이터인데 DB에서 가져온 데이터를 왜 사용하지 않는 걸까?

이유는 영속성 컨텍스트가 영속 상태인 엔티티의 동일성을 보장해야하기 때문이다.

  1. DB에 있는 Post("hibernate")를 새로운 엔티티로 추가한다?
    -> 기존의 엔티티와 @Id 중복이 발생
  2. 영속성 컨텍스트에 있는 Post("hi") -> Post("hibernate")로 대체?
    -> 영속성 컨텍스트에 수정중인 데이터가 유실될 수 있어 위험

따라서, DB에서 조회된 결과를 버리고 기존 엔티티를 반환한다.


@Modifying(clearAutomatically = true)

이러한 문제로 인해 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를 한 조회 결과가
모두 영속성 컨텍스트에 저장된다.

다시 테스트를 실행하면 두 케이스 모두 성공한다.


References

profile
얍얍 개발 펀치

0개의 댓글