JPQL과 영속성 컨텍스트의 관계
📌 개요
JPQL
은 일반적으로 영속성 컨텍스트를 고려하지 않고 데이터베이스에 접근하여 작업을 수행한다. 영속성 컨텍스트와 어떠한 상호작용을 하는지는 JPQL이 어떤 쿼리인지에 따라 다르다.
📌 JPQL - SELECT 쿼리
만약 JPQL이 SELECT
쿼리였다면 실행하기 전 flush()
를 수행하여 데이터 일관성을 맞춘다.
SELECT 쿼리를 실행하기 전 flush()
를 수행하지 않는다고 하면 어떤 일이 발생할까? 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 아직 DB에 반영되지 않은 쿼리가 있을 수 있으며, SELECT 쿼리로 가져온 데이터와 영속성 컨텍스트의 데이터를 다를 수 있다.
전체적인 과정은 다음과 같다.
- JPQL이 SELECT 쿼리를 날리기 전,
flush()
를 수행한다. - JPQL이 DB에 쿼리를 날려 결과를 가져온다.
- 가져온 엔티티는 영속성 컨텍스트에서 관리된다. 식별자를 기준으로 영속성 컨텍스트의 1차 캐시에 해당 엔티티가 이미 존재하는지 확인한다.
- 만약 DB에서 가져온 엔티티가 영속성 컨텍스트의 엔티티와 동일하다면 DB에서 가져온 엔티티를 버리고 1차 캐시의 엔티티를 그대로 리턴한다.
📌 JPQL - UPDATE, DELETE 쿼리
만약 JPQL이 UPDATE
, DELETE
쿼리였다면 영속성 컨텍스트를 우회하여 DB에 직접 쿼리를 실행하게 된다. 즉, dirty checking
이 동작하지 않는다.
더 구제적으로 어떤 문제가 발생하는지 살펴보자. JPQL로 특정 엔티티를 변경하였다고 가정하자. 하지만 이전에 영속성 컨텍스트의 1차 캐시에 저장된 엔티티가 존재한다면 해당 엔티티는 JPQL 연산의 영향을 받지 않으므로 변경 전 상태를 유지하게 된다. 즉, 1차 캐시의 데이터와 DB의 데이터 간 불일치가 발생하게 된다.
이와 같은 데이터 불일치 문제를 해결하기 위해서 영속성 컨텍스트를 수동으로 초기화해야 한다. 예를 들어 em.clear()
연산을 수행하여 1차 캐시를 초기화하는 방법이 있다.
그렇다면 JPQL의 UPDATE, DELETE 쿼리는 사용하면 안 되는가?
그렇지 않다. JPQL의 UPDATE, DELETE 쿼리는 대량의 데이터를 처리하는 데 매우 효과적이다. JPQL의 UPDATE, DELETE 쿼리를 bulk operation
이라고 부르기도 한다.
수천 개의 엔티티를 변경하는 상황이 주어졌다고 가정하자. 만약 dirty checking 방법을 택한다면 수천 개의 엔티티를 메모리에 로드해야 하며, 각각의 엔티티에 대해 UPDATE 쿼리를 실행해야 할 것이다. 즉, 수천 개의 UPDATE 쿼리가 실행되게 되는 것이다.
그러나 JPQL을 사용한다면 쿼리를 작성하여 단 하나의 쿼리만으로 모든 레코드를 한 번에 수정할 수 있다. 엔티티 또한 메모리에 로드하지 않고 작업을 수행한다.
정리하면, JPQL 벌크 연산은 대량의 데이터를 효율적이고 일괄적으로 처리해야 하는 상황에서 유리한 선택지이다. 반대로 얘기하면, dirty checking은 소수의 엔티티를 수정하는 경우 효과적이다.
📌 마치며
JPQL이 SELECT 쿼리였다면 치명적인 문제는 발생하지 않으나 UPDATE, DELETE 쿼리였다면 상황에 따라 큰 문제가 발생할 수 있다. JPQL로 쿼리를 작성하는 경우 위 점을 잘 인지하고 작성해야겠다.