Bulk 연산 시 UpdatedAt 필드가 자동으로 갱신되지 않는다?
배경
저는 진행 중인 프로젝트에서 채팅 기능을 담당하고 있습니다.
기획에 따르면, 일주일이 지난 채팅 요청은 자동 거절되어야 합니다.
이를 구현하기 위해 스케줄러를 돌려 채팅 요청들의 상태를 bulk update 해주었습니다.
채팅요청 상태가 대기중이고, 현재 시간을 기준으로 생성된 지 일주일이 지났을 때
채팅요청 상태를 거절됨으로 변경해 주는 코드입니다.
public void updateChatInquiryStatusRejected() {
queryFactory.update(chatInquiry)
.set(chatInquiry.status, InquiryStatus.REJECTED)
.where(
chatInquiry.status.eq(InquiryStatus.PENDING) // 대기중
chatInquiry.createdAt.loe(LocalDateTime.now().minusWeeks(1)), // 일주일 지남
)
.execute();
}
자동 거절된 채팅 요청들은 updatedAt이 createdAt보다 7일 늦어야 합니다.
앞에서 설명했듯이 생성일로부터 7일 뒤에 요청상태가 변경되기 때문이죠.
하지만 DB를 확인해보니 createdAt과 updatedAt이 동일했습니다.
채팅 상태는 업데이트 되었지만, updatedAt 필드가 함께 업데이트되지 않은 것입니다.
참고로 JPA Auditing을 사용하여 createdAt과 updatedAt 필드를 자동으로 관리하였습니다.
따라서 따로 updatedAt 필드를 직접 업데이트하지 않아도 updatedAt 필드가 업데이트되어야 합니다.
이에 저는 bulk update를 진행할 경우 updatedAt이 자동 반영되지 않는 걸까 의문이 들었습니다.
이를 확인하기 위해 간단한 bulk update문을 작성하고 테스트해 보았습니다.
Bullk Update 수행 시 updatedAt 반영 여부
회원의 상태를 일괄 업데이트하는 코드입니다.
public void bulkUpdate(Status status, LocalDateTime updatedAt) {
queryFactory.update(member)
.set(member.status, status)
.execute();
em.clear();
}
em.clear를 통해 벌크 연산 이후 1차 캐시를 비우도록 하였습니다.
@DisplayName("[bulk 업데이트 시 updatedAt이 갱신되지 않는다.]")
@Test
void querydsl_test() {
//given
Member member = memberRepository.findById(1L).orElseThrow();
log.info("벌크 업데이트 전 updatedAt={}", member.getUpdatedAt());
//when
memberRepository.bulkUpdate(Status.B);
//then
member = memberRepository.findById(1L).orElseThrow();
log.info("벌크 업데이트 후 updatedAt={}", member.getUpdatedAt());
}
벌크 연산 후 1차 캐시를 비웠기 때문에 member를 DB에서 직접 조회하게 됩니다.
로그를 보면 벌크 업데이트 후에 member를 다시 조회했을 때, updatedAt이 변경되지 않았습니다.
변경 감지 시 updatedAt 반영 여부
그렇다면 변경 감지로 업데이트되었을 때는 updatedAt이 잘 반영될까요?
member를 DB에서 다시 조회하기 위해 1차 캐시에 있던 쿼리문을 DB로 flush한 후 1차 캐시를 비워주었습니다.
@DisplayName("[변경감지로 업데이트 시 updatedAt이 자동 갱신된다.]")
@Test
void dirty_check_test() {
//given
Member member = memberRepository.findById(1L).orElseThrow();
log.info("변경감지 전 updatedAt={}", member.getUpdatedAt());
//when
member.setStatus(Status.B);
em.flush();
em.clear();
//then
member = memberRepository.findById(1L).orElseThrow();
log.info("변경감지 후 updatedAt={}", member.getUpdatedAt());
}
변경 감지로 상태가 업데이트될 시 updatedAt 필드도 자동으로 업데이트됨을 확인하였습니다.
Bulk Updated 시에만 updatedAt이 반영되지 않는 원인
왜 벌크 업데이트를 진행할 시에만 updatedAt이 반영되지 않는 걸까요?
이는 JPA Auditing 기능은 영속성 컨텍스트 내에만 작동되기 때문입니다.
JPA Auditing은 영속성 컨텍스트에 관리되는 엔티티가 수정될 때 자동으로 updatedAt 필드를 업데이트합니다.
그러나 벌크 연산은 영속성 컨텍스트를 우회하고 DB에 직접 쿼리하기 때문에 Auditing 기능이 작동하지 않습니다.
따라서 벌크 연산을 사용하는 경우, updatedAt을 직접 넣어주어야 하는 것입니다.
해결
Bulk Update 함수에 파라미터를 추가해 updatedAt을 수동으로 업데이트해 주었습니다.
public void bulkUpdate_querydsl_with_updatedAt(Status status, LocalDateTime updatedAt) {
queryFactory.update(member)
.set(member.status, status)
.set(member.updatedAt, updatedAt)
.execute();
em.clear();
}
memberRepository.bulkUpdate_querydsl_with_updatedAt(Status.B, LocalDateTime.now());
bulk 함수 호출 전후로 로그를 찍어보면, updatedAt 필드가 갱신되었음을 확인할 수 있습니다.
느낀 점
JPA Auditing 기능이 편리하여 매번 사용하였지만, 영속성 컨텍스트 내 관리되는 엔티티에만 적용됨을 알지 못했습니다.
이 경험을 통해 스프링에서 제공하는 기능을 단순히 사용하는 게 아니라 작동 원리를 알아야겠다고 느꼈습니다.