JPA 를 다시 공부하다가 변경감지를 내가 잘 이해하지 못했던 것 같아 정리해본다.
Dirty Checking 동작 방식
JPA 의 영속성컨텍스트(Entitymanger)에는 1차 캐시와 쓰기 지연 SQL 저장소가 존재한다.
1차 캐시에는 id와 entity가 저장되고 쓰기 지연 저장소에는 쿼리가 쌓이는데 commit 하는 시점에 DB에 반영이 된다.
해서, 1차 캐시에 저장되어있는 엔티티를 수정하면 Dirty Checking(변경 감지) 으로 인해 쓰기 지연 저장소에 update 쿼리가 쌓이고 Transaction 이 끝나는 시점에 DB에 반영이 된다.
자바 코드로 commit이나 flush를 통해 커밋하는 게 아닌 테스트 시나리오 상에서 update 쿼리가 언제 호출되는지 보고싶어서 테스트 코드를 작성해보았다.
@BeforeEach
public void setup(){
Member member = Member.builder()
.id(1L)
.email("limnj@test.com")
.name("홍길동")
.passwd("1234")
.address("서울특별시")
.tel("010-1234-5678")
.build();
memberRepository.save(member);
}
@Test
@Transactional
@DisplayName("회원 정보 수정 테스트-성공")
public void givenMemberObject_whenUpdateMember_thenReturnMemberObject() {
// given - precondition or setup
MemberDto memberDto = MemberDto.builder()
.name("이름")
.passwd("12345")
.address("서울특별시")
.tel("010-1234-5679")
.build();
// when - action or the behaviour that we are going test
memberServiceImpl.updateMember(1L, memberDto);
// then - verify the output
Optional<Member> findMember = memberRepository.findById(1L);
findMember.ifPresent(
m -> {
assertThat(m.getPasswd()).isEqualTo(memberDto.getPasswd());
assertThat(m.getName()).isEqualTo(memberDto.getName());
assertThat(m.getTel()).isEqualTo(memberDto.getTel());
assertThat(m.getAddress()).isEqualTo(memberDto.getAddress());
}
);
}
BeforeEach로 member data를 저장하고 테스트를 실행하였다.
테스트는 성공이고, 내 생각에는 여기서 insert 이후 update 쿼리가 실행되어야하는데 보이지않았다.

왜일까 ? 1차 캐시와 test 코드의 @Transactional 때문이다.
먼저 MemberService 와 Test Code 에 모두 @Transactional 이 있기 때문에 영속성 컨텍스트를 공유한다.
해서, 사전에 저장한 member data 는 1차 캐시에 저장되어 있고 updateMember 메서드에서 findById를 호출하면 식별자를 통해 entity를 조회하기 때문에 1차 캐시에서 조회된다. 그렇기 때문에 select 쿼리도 찍히지 않고 변경된 값은 1차 캐시에 저장되기 때문에 다시 조회를 했을 때의 findMember 값은 DB에 반영된 값이 아닌 1차 캐시 값이라 마찬가지로 조회 쿼리가 찍히지 않는다.
즉, 지금은 DB에 반영되지 않은 상태이고 findAll 을 통해 모든 member data를 조회하면 쓰기 지연 SQL 저장소의 update 쿼리가 DB에 반영되는 것을 확인할 수 있다.
test 코드에 @Transactional 을 빼고 실행하면 좀 더 명확히 확인할 수 있다.

Dirty Checking 성능 개선
그런데 만약 10명의 회원이 있고 이 중 3명의 전화번호와 주소만 값이 바뀌었다.
이 때, Dirty Checking 으로 발생한 update query 를 보면 모든 인스턴스의 모든 필드에 대한 update query 가 호출되었음을 확인할 수 있다. 이는 추후 회원이 많아질수록 느려지는 원인이 될 수도 있지않을까 싶었다.
해서, 값이 변경된 인스턴스의 변경된 필드에 한해서만 update query 가 호출될 수 있도록 하는 방법은 매우 간단하다.
해당 클래스의 @DynamicUpdate 만 붙여주면 된다.
@DynamicUpdate
public class Member {
...
}
이후 위에 말했던 시나리오대로 10명의 회원을 저장해두고 그 중 3명의 전화번호와 주소 값만 바꿔보았다.
결과는 다음과 같으며 변경된 값에 한해서만 update query를 호출하는 것을 확인할 수 있다.

정리
JPA 의 쿼리를 확인하고 성능을 체크하는 것이 중요한 것 같다.
또한, 스스로를 되돌아보면 테스트 코드의 @Transactional 을 사용했을 때 데이터가 롤백된다는 장점보다는 service layer 이기 때문에 나도 모르게 썼던 것 같다.
왜 쓰는지에 대한 의문을 갖고 사용하도록하자.
참고
https://javatute.com/hibernate/dirty-checking-in-hibernate/#google_vignette
'Web' 카테고리의 다른 글
| [Spring] CORS Configuration with Spring security (0) | 2023.09.17 |
|---|---|
| [Spring] 동시성 이슈와 synchronized 에 관하여 (0) | 2023.09.10 |
| [Spring] Maven Scope 와 Gradle Configurations (0) | 2023.08.27 |
| [Spring] compiletime 과 runtime 에 대해서 (0) | 2023.08.13 |
| [Spring] XSS Prevention (2) | 2023.07.31 |