JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인
이 테스트는 두 가지 주요 목적을 가지고 있습니다:
- JPQL 실행 전
flush가 실행되는지 확인합니다. - 영속성 컨텍스트에 동일한 ID를 가진 엔티티가 존재할 경우, JPQL로 가져온 값이 영속성 컨텍스트의 값으로 대체되지 않는지 확인합니다.
- 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);
}
테스트 설명
Member 저장
Member member = saveMemberAndMemberImage(); Long memberId = member.getId();이 메서드에 의해
member와image가 저장됩니다.@GeneratedValue(strategy = GenerationType.IDENTITY)로 설정되어 있기 때문에member가 저장될 때 바로 저장 쿼리가 실행됩니다. 이 상태에서 영속성 컨텍스트와 데이터베이스 모두에member가 존재합니다. 이때Member엔티티의isWithdrawal필드는 데이터베이스에 의해 기본값으로 초기화되지만, 해당필드의 초기화 상태가 영속성 컨텍스트에는 반영되지 않습니다.
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를 조회합니다. 이때 JPQLmemberQueryService.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에 의해 기본값으로 초기화된 엔티티가 현재 영속성 컨텍스트 안의 엔티티로 대체되지 않습니다.)
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 이라는 필드가 있는데, 아직 영속성 컨텍스트 안에 있는 entity 는 clear() 가 된 적이 없고, 따라서 DB에 의해 초기화된 필드는 영속성 컨텍스트 안에 적용이 되지 않았을 것입니다.

이를 검증하기 위해 아래와 같이 최종적으로 찾은 updatedMember 의 isWithdrawal 필드를 가져와보면 null 인것을 확인할수 있으며
만약 isWithdrawal 이 초기화된 상태를 보고싶다면 movkMvc.perform() 메서드 호출 이전 또는 이후에 entityManager 의 clear 메서드를 실행해주면 잘초기화된 isWithdrawal 값을 볼수있습니다.


참고로 mockMvc.perform 이후에 clear() 를 호출하는 로직에서 entityManger.flush() 가 추가된 이유는 만약 entityManger.clear() 만 호출시 mockMvc.perform() 에 의해 업데이트된 member 엔티티의 변경사항이 모두 detach 되면서 업데이트 쿼리가 나가지 않습니다. 따라서 mockMvc.perform 이후에는 entityManger.flush() 를 추가해 member 엔티티의 update 정보가 사라지지 않도록 합니다.
결론
JPQL이 실행되기 전에
flush가 실행되는지 확인:mockMvc.perform()이후,JPQL로 인해update쿼리가 실행되는 것을 확인했습니다. 이는JPQL이 실행되기 전에flush가 자동으로 호출된다는 것을 의미합니다.
영속성 컨텍스트에 동일한 ID의 엔티티가 존재할 때 JPQL로 가져온 값이 대체되지 않는지 확인:
entityManager.clear()를 호출하지 않으면,JPQL로 가져온 값이 영속성 컨텍스트에 이미 존재하는 값으로 대체되지 않는다는 것을 확인했습니다. 예를 들어,member.getIsWithdrawal()이null인 것을 확인했습니다.
사실, 엔티티 클래스 안에 초기화 로직을 넣거나 다른 방법으로 영속성 컨텍스트와 데이터베이스 간의 불일치를 줄이는 것이 가장 좋은 방법이지만, 이번 테스트는 이 불일치를 확인하기 위해 수행되었습니다.
'Spring' 카테고리의 다른 글
| Spring WebSocket(STOMP) 통합 테스트 정리 (0) | 2025.10.17 |
|---|---|
| Flyway 사용법 (#1) (0) | 2025.09.25 |
| API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점 (0) | 2024.07.22 |
| JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
| [Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |