JPA : JPQL과 영속성 컨텍스트

    이 게시글은 자바 표준 ORM JPA 프로그래밍을 공부하고 정리한 글입니다. 


    JPQL은 엔티티만 영속화 된다.

    JPQL로 DB에서 데이터를 불러온다고 해보자. 불러올 수 있는 데이터는 값타입, 엔티티 타입, 임베디드 타입 등 다양한 값이 있을 수 있다. JPQL로 불러올 수 있는 이 데이터들 중에서 '엔티티 타입'만 유일하게 영속성 컨텍스트에서 관리가 된다. 즉, 다른 스칼라 타입같은 것들은 영속화 되지 않기 때문에 쓰기 지연, 더티 체킹 등의 기능이 정상적으로 동작하지 않는다. 

     


    em.find()의 동작 방식

    쌩뚱 맞을 수 있지만, em.find()의 동작방식을 한번 복습해본다. 왜냐하면 em.find()와 JPQL은 다른 방식으로 동작하기 때문이다.  em.find()의 동작 방식은 아래 그림에서 볼 수 있다.

    1. em.find 대상을 영속성 컨텍스트 1차 캐시에서 조회한다.
    2. 1차 캐시에 있으면 엔티티를 반환한다. 없으면 DB 조회한다.
    3. DB에서 조회한 값이 반환되면 영속성 컨텍스트에 영속화 한다.
    4. 영속화 된 엔티티를 반환한다. 

    여기서 눈여겨 봐야할 부분은 em.find()는 반드시 1차 캐시에 원하는 엔티티가 있는지를 확인한다는 점이다. 이 점을 유의하고 JPQL의 동작 방식을 확인해보자.

     


    JPQL의 동작 방식

    JPQL은 영속성 컨텍스트를 거치지 않고 바로 DB에 쿼리를 보낸다. 이런 동작 방식으로 인해 영속성 컨텍스트의 쓰기 지연 저장소에 있는 값이 반영되지 않을 경우, DB의 데이터 정합성이 떨어진다. 따라서 JPQL은 쿼리를 보내기 전에 반드시 em.flush()가 자동으로 실행된다.

    1. JPQL 쿼리 대상을 DB에서 바로 조회한다.
    2. DB에서 조회한 엔티티를 영속성 컨텍스트로 반환한다.
    3. 영속성 컨텍스트에 있던 엔티티면 DB에서 조회한 값을 버린다. 없던 엔티티면 영속화 한다.
    4. 영속화 된 엔티티를 반환한다.

    JPQL은 em.find()와 다르게 DB에 바로 쿼리가 나간다. 이런 이유로 약간은 다르게 동작하는 부분도 있다. 

     


    em.find() vs JPQL 쿼리

    em.find()는 영속성 컨텍스트의 1차 캐시를 먼저 참고한다. JPQL은 1차 캐시를 참고하지 않고 바로 DB에 값을 확인하는 과정을 가진다. 이 때, JPQL은 찾아온 엔티티가 이미 영속성 컨텍스트에 있을 경우 버리는 전략을 취한다. 왜 JPQL은 굳이 찾아온 엔티티를 버릴까? 

    JPQL이 찾아온 엔티티를 버리는 이유는 간단하다. 이미 영속화 되어있던 엔티티가 수정 중인 경우가 있을 수 있기 때문이다. 이 이유 때문에 동일한 엔티티를 가리킨다면, 기존의 엔티티를 그대로 보존하는 방법이 더 안정성이 있다. 이런 이유로 JPQL은 새로 조회했지만 기존에 영속성 컨텍스트에 있던 엔티티를 버리는 전략을 한다. 


    JPQL에 대한 정리

    1. JPQL로 찾아온 엔티티는 모두 영속화 되어 있다.
    2. JPQL로 찾아온 엔티티 중 이미 영속성 컨텍스트에 있는 것이 있다면 버린다.
    3. JPQL은 항상 DB를 먼저 조회한다. 

     


    JPQL과 Flush 모드

    JPQL은 항상 DB를 먼저 조회한다. 따라서 JPQL 쿼리를 하기 전에 DB와 영속성 컨텍스트의 동기화 작업이 필요해서, 항상 Flush()가 발생한다. 예를 들어 영속성 컨텍스트에는 2천원이 수정되어 변경 감지를 기다리고 있고, DB에는 3천원이라고 가정해보자. 이 경우, Flush() 되지 않으면 엉뚱한 값을 불러온다. 따라서 JPQL은 실행 전 항상 Flush()를 해준다.

    문제는 Flush()가 너무 잦은 경우가 있을 수 있다. 예를 들어 위와 같은 경우가 있다고 해보자. 그런데 쿼리 해오는 엔티티들 간의 상관관계가 없다고 해보자. 이 경우 DB와 영속성 컨텍스트의 데이터를 동기화 할 필요는 없다. 아시다시피 DB 연결을 하는 것은 정말 많은 비용을 소모하기 때문에 Flush()를 줄이면서 성능을 최적화 할 수 있다. 

     


    JPQL 플러시 모드 설정

    queryFactory.selectFrom(member)
            .setFlushMode(FlushModeType.AUTO)
            .fetch();
    
    queryFactory.selectFrom(member)
            .setFlushMode(FlushModeType.COMMIT)
            .fetch();​

    JPQL(QUERY DSL)은 setFlushMode를 이용해서 플러시 모드를 설정할 수 있게 해준다. 예를 들어 Flush Mode가 Auto인 경우, JPQL 쿼리를 사용할 때 마다 Flush()가 된다. 즉, 그냥 JPQL을 쓰는 것처럼 한다(Auto가 Default). 반면 FlushModeType을 Commit으로 설정할 수 있다. Commit으로 설정하게 되면 JPQL이 나갈 때, Flush()가 발생하지 않는다. 오직 트랜잭션이 Commit 하는 시점에만 Flush()가 되도록 한다.

    'Spring > JPA' 카테고리의 다른 글

    OneToMany에서 CascadeType.ALL의 N+1 문제 야기  (2) 2023.11.18
    JPA : PK 생성 전략  (0) 2022.05.23
    JPA : Bulk 연산의 주의할 점  (0) 2022.02.23
    JPA : Collection과 JPA 동작 방식  (1) 2022.02.23
    JPA : 2차 캐시  (0) 2022.02.23

    댓글

    Designed by JB FACTORY