JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인

JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인

  1. 이 테스트는 두 가지 주요 목적을 가지고 있습니다:

    1. JPQL 실행 전 flush가 실행되는지 확인
    2. 영속성 컨텍스트에 동일한 ID를 가진 엔티티가 존재할 경우, JPQL로 가져온 값이 영속성 컨텍스트의 값으로 대체되지 않는지 확인

 

@Test
@DisplayName("닉네임 변경 요청시 해당 회원의 닉네임을 변경한다.")
void updateMemberWithNickNameRequest() throws Exception {
  //given
  Member member = saveMemberAndMemberImage();
  Long memberId = member.getId();

  Jwt jwtCreatedBySavedMember = generateTokenWithMemberId(memberId);

  MemberUpdateRequest memberUpdateRequest = MemberUpdateRequest.builder()
    .nickname(CHANGED_NICK_NAME)
    .build();

  String jsonRequest = objectMapper.writeValueAsString(memberUpdateRequest);

  //when & then
  mockMvc.perform(
      MockMvcRequestBuilders.patch("/api/members/{memberId}", memberId)
        .header(AUTHORIZATION_STRING, JWT_TOKEN_PREFIX + jwtCreatedBySavedMember.getAccessToken())
        .contentType(MediaType.APPLICATION_JSON)
        .content(jsonRequest))
    .andExpect(status().isOk());

  Member updatedMember = memberProvider.findById(memberId);
  assertThat(updatedMember.getNickname()).isEqualTo(CHANGED_NICK_NAME);
}

 

테스트 설명

  1. Member 저장

    Member member = saveMemberAndMemberImage();
    Long memberId = member.getId();
    

    이 메서드에 의해 memberimage가 저장됩니다. @GeneratedValue(strategy = GenerationType.IDENTITY)로 설정되어 있기 때문에 member가 저장될 때 바로 저장 쿼리가 실행됩니다. 이 상태에서 영속성 컨텍스트와 데이터베이스 모두에 member가 존재합니다. 이때 Member 엔티티의 isWithdrawal 필드는 데이터베이스에 의해 기본값으로 초기화되지만, 해당필드의 초기화 상태가 영속성 컨텍스트에는 반영되지 않습니다.

 

  1. MockMvc 요청

    mockMvc.perform(
            MockMvcRequestBuilders.patch("/api/members/{memberId}", memberId)
                .header(AUTHORIZATION_STRING, JWT_TOKEN_PREFIX + jwtCreatedBySavedMember.getAccessToken())
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonRequest))
        .andExpect(status().isOk());
    

    이 요청은 member의 닉네임을 변경합니다. updateMember 메서드는 JPQL을 통해 member를 조회합니다. 이때 JPQL memberQueryService.findById(memberId) 이 실행되기 전에 flush가 자동으로 실행됩니다.

    @Transactional
    public void updateMember(Long memberIdFromJwt, Long memberId, MemberUpdateRequest memberUpdateRequest) {
        validateMemberId(memberIdFromJwt, memberId);
        Member member = memberQueryService.findById(memberId); // JPQL 실행 전 flush() 호출
    
        Optional.ofNullable(memberUpdateRequest.getAlert())
            .ifPresent(member::updateAlert);
    
        Optional.ofNullable(memberUpdateRequest.getNickname())
            .ifPresent(member::updateNickname);
    }
    

    entityManager.clear()를 호출하지 않았으므로, entityManager에는 여전히 saveMemberAndMemberImage() 메서드에 의해 저장된 member가 존재합니다. 이 상태에서 JPQL로 가져온 member는 영속성 컨텍스트에 이미 존재하는 member로 대체되지 않습니다.

    (JPQL 을 통해 값을 가져오더라도, 영속성 컨텍스트안에 같은 ID 를 가진 객체가 있다면 JPQL 을 통해 가져온 값을 버리고 영속성 컨텍스트 안에 값을 유지하기 때문에 DB에 의해 기본값으로 초기화된 엔티티가 현재 영속성 컨텍스트 안의 엔티티로 대체되지 않습니다.)

 

  1. Update 와 Flush

    Member updatedMember = memberProvider.findById(memberId);
    assertThat(updatedMember.getNickname()).isEqualTo(CHANGED_NICK_NAME);
    

    이 코드에서 memberProvider.findById(memberId)는 JPQL 쿼리를 실행하며, 쿼리 실행 전에 flush가 호출됩니다. flush 가 호출될때, mockMvc.perform() 에 의해 실행된 Member 엔티티 업데이트 쿼리가 나갈것입니다. 트랜잭션의 전파에 의해 test 코드에 걸어놓은 트랜잭션이 끝나지 않았으므로 mockMvc.perform 에 의해 실행된 update 쿼리는 mockMvc.perform 이 실행이 완료된 후에도 아직 실행되지 않기 때문입니다.

 

❗️이때 중요한것은 우린 아직 entityManager.clear() 를 한번도 호출하지 않았다는 점입니다. Member 엔티티 안에는 DB의 기본값에 의해 초기화되는 isWithdrawal 이라는 필드가 있는데, 아직 영속성 컨텍스트 안에 있는 entityclear() 가 된 적이 없고, 따라서 DB에 의해 초기화된 필드는 영속성 컨텍스트 안에 적용이 되지 않았을 것입니다.

이를 검증하기 위해 아래와 같이 최종적으로 찾은 updatedMemberisWithdrawal 필드를 가져와보면 null 인것을 확인할수 있으며

 

updatedMembergetisWithdrawalnull

 

만약 isWithdrawal 이 초기화된 상태를 보고싶다면 movkMvc.perform() 메서드 호출 이전 또는 이후에 entityManagerclear 메서드를 실행해주면 잘초기화된 isWithdrawal 값을 볼수있습니다.

entityManagerFlushAfterMvcPerform

entityManagerClearAfterMvcPerform

참고로 mockMvc.perform 이후에 clear() 를 호출하는 로직에서 entityManger.flush() 가 추가된 이유는 만약 entityManger.clear() 만 호출시 mockMvc.perform() 에 의해 업데이트된 member 엔티티의 변경사항이 모두 detach 되면서 업데이트 쿼리가 나가지 않습니다. 따라서 mockMvc.perform 이후에는 entityManger.flush() 를 추가해 member 엔티티의 update 정보가 사라지지 않도록 합니다.

 

결론

  1. JPQL이 실행되기 전에 flush가 실행되는지 확인:

    • mockMvc.perform() 이후, JPQL로 인해 update 쿼리가 실행되는 것을 확인했습니다. 이는 JPQL이 실행되기 전에 flush가 자동으로 호출된다는 것을 의미합니다.
  2. 영속성 컨텍스트에 동일한 ID의 엔티티가 존재할 때 JPQL로 가져온 값이 대체되지 않는지 확인:

    • entityManager.clear()를 호출하지 않으면, JPQL로 가져온 값이 영속성 컨텍스트에 이미 존재하는 값으로 대체되지 않는다는 것을 확인했습니다. 예를 들어, member.getIsWithdrawal()null인 것을 확인했습니다.

       

사실, 엔티티 클래스 안에 초기화 로직을 넣거나 다른 방법으로 영속성 컨텍스트와 데이터베이스 간의 불일치를 줄이는 것이 가장 좋은 방법이지만, 이번 테스트는 이 불일치를 확인하기 위해 수행되었습니다.

+ Recent posts