1차 캐시 데이터 vs DB 데이터

Patrick YOO·2022년 1월 30일
1
post-thumbnail

서론

JPA 사용시 복잡한 쿼리를 작성할때 JPQL 또는 QueryDSL 을 사용하게 된다.
JPQL 또는 QueryDSL 의 공통점은 1차 캐시에 저장되어있는 객체를 조회 하는것이 아닌 다이렉트로 DB에서 데이터를 조회하게 된다. 이 경우에서 일어날 수 있는 문제를 짚고 넘어가고자 한다.

QueryDsl 과 JPQL 가장큰 특징

  • QueryDsl 과 JPQL 은 DB에 select SQL 을 다이렉트로 쏜후 데이터를 조회해온다.
  • 영속성 컨텍스트 변경감지 영역에 SQL 을 저장하는 것이 아닌 update SQL 문 작성후 실행시 그 즉시 sql 문을 db 에 flush 한다.
    public void updateMember(){

        jpaQueryFactory.update(memberMst)
                .set(memberMst.username,"Changed")
                .where(memberMst.id.goe(3))
                .execute();

    }

위 코드를 실행해보도록 하자.

    @Transactional
    public void updateMember(){
        memberQueryRepository.updateMember();
        log.info("실행완료");
    }

위 코드 실행 즉시 아래와 같이 SQL문을 flush 한다.

발생할 수 있는문제

영속성 컨텍스트에 이미 존재하는 객체와 DB에서 조회해온 객체 아이디가 같을 경우 1차 캐시에 존재하는 데이터를 사용한다.

발생할 수있는 문제를 살펴보자
-[STEP1] : 디비에 있는 전체 맴버를 조회 해온다.

현재 디비에 들어가있는 Member Data 는 아래와 같다.

-[STEP2] : 디비에 있는 전체 username 을 Changed 로 바꾼다.

-[STEP3] : 디비에 있는 데이터를 다시 조회 한후 변화된 데이터를 살펴본다.

   @Transactional
    public void updateMember(){
	//[STEP1] : 디비에 있는 멤버를 전체 조회 해온다.	
        List<MemberMst> members = memberQueryRepository.findAllMembers();
        
	//[STEP2] : 디비에 있는 전체 멤버의 이름을 Changed 로 바꾼다.
        memberQueryRepository.updateMember();
        log.info("실행완료");
        //[STEP3] : 디비에서 직접 조회 한 데이터를 살펴본다.
    	List<MemberMst> membersAfterUpdate = memberQueryRepository.findAllMembers();    
        membersAfterUpdate.forEach(System.out::println);
    }
---------------------------------------------------------------------------        
    // [STEP1] : 멤버 전체 조회
    public List<MemberMst> findAllMembers(){
        return jpaQueryFactory.selectFrom(memberMst).fetch();
    }


    //[STEP2]
    public void updateMember(){
        jpaQueryFactory.update(memberMst)
                .set(memberMst.username,"Changed")
                .execute();
    }

위 모든 코드가 종료 되었을때 어떤 데이터가 찍히는지 살펴보자.

😕 데이터를 살펴보니 update 문이 전혀 반영이 되지 않았다.

원인

  • QueryDsl 과 JPQL 은 실행 순간 DB에 SQL 문을 쏴서 디비를 업데이트를 하는건 사실이다 하지만 영속성 컨텍스트에 동일한 PK 가 존재할 경우 1차 캐시에 존재하는 객체가 우선권을갖게 되며 DB에서 조회하는 데이터를 버린다.
    그렇게 될경우 만약 위에서 업데이트 한 맴버들중 몇개를 아래에서 다시 수정하게 될경우 업데이트를 실행후 바뀐 변경사항이 무시될 수 있다.

코드를 살펴보자.

    @Transactional
    public void updateMember(){

        List<MemberMst> members = memberQueryRepository.findAllMembers();

        memberQueryRepository.updateMember();
        log.info("실행완료");

        List<MemberMst> membersAfterUpdate = memberQueryRepository.findAllMembers();
        membersAfterUpdate.forEach(System.out::println);

        membersAfterUpdate.get(0).changeAge(55);

    }
    
        public void updateMember(){
        jpaQueryFactory.update(memberMst)
                .set(memberMst.username,"Changed")
                .execute();
    }
  • 5명의 맴버 전원의 이름을 Changed 로 바꾼후 첫번째 맴버의 나이를 55살로 바꾸는 코드이다
    결과를 살펴보자.

나머지 멤버의 이름이 정상적으로 변경되었지만 1번 맴버는 나이가 변경되었고 이름은 변경되지 않았다.
이는 첫번째 멤버의 나이를 변경할때 영속성컨텍스트에 있는 첫번째 멤버를 기준으로 하여 변경감지가 일어났기 때문에다.

개선방안

  • update 연산후 1차캐시에 남은 데이터를 싹 비워주어 다시 select 해줄 수 있다. 그렇다면 1차캐시에 데이터가 비어있으니 새로이 select 해온 값으로 대체할수 있다.

다시 테스트 해보자

public void updateMember(){
        jpaQueryFactory.update(memberMst)
                .set(memberMst.username,"Changed")
                .execute();
        entityManager.flush();
        entityManager.clear();
    }

연산후 1차 테스트와 다른 결과가 나오는지 확인해보자.

이제는 첫번째 맴버인 6번까지 username 이 정상적으로 바뀌는것을 볼 수 있다.

결론

  • QueryDSL JPQL 조회시 DB에 있는 데이터를 조회해 올지라도 1차캐시에 존재하는 동일한 식별자를 가진 객체가 있으면 1차 캐시가 절대적으로 우선권을 가진다.
  • 벌크연산 또는 update 문이 진행했을때 상황에 따라 영속성 컨텍스트를 비워주는 작업이 필요할 수 있다.
    반드시 업데이트 문을 실행했을때 영속성 컨텍스트를 비워라! 는 아니다. 1차캐시와 2차캐시 디비 조회의 데이터가 어떻게 조화를 이룰지 잘 생각해보면서 개발을 하자

References

  1. https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84
profile
자유인을 꿈꾸는 개발자

0개의 댓글