A -> B (1:n), B -> C (1:n), B -> D (1:n) 으로 엔티티가 연결된 상황이다.
Experience fresh = experienceRepository.findById(experience.getId()).orElseThrow();
fresh.setSummary(summary);
experienceRepository.save(fresh);
다음 로직과 같이, findById로 변경사항을 저장할 엔티티를 선택하고, 해당 엔티티의 필드값을 set하여 값을 변경하고자 했다.
A의 필드를 변경하고 save
하면, 단순히 UPDATE 하나의 쿼리가 발생할 것이라고 생각했다.
하지만 다음과 같이 5개 쿼리가 발생했다
1. A엔티티 단건 조회
2. A, B fetch join 조회
3. B와 연관된 C 조회
4. B와 연관된 D 조회
5. A 업데이트 쿼리
A엔티티에서만 UPDATE가 일어나야하는데 A엔티티와 연관된 B, C, D 엔티티까지 조회하는 상황이었다.
디버깅 결과, save호출 시 추가 쿼리 4개가 발생함을 확인할 수 있었다.
명시적으로 repository.save를 사용한 이유는 @Async을 사용하여 해당 로직을 비동기로 만들었기 때문이었다.
하지만 save
를 호출했을 때 연관된 엔티티들까지 추가로 조회되었기 때문에, 해당 로직을 수정할 필요가 있었다.
처음에는 CascadeType.ALL
설정 때문이라고 생각했다.
hibernater가 연관된 엔티티 상태를 확인하기 위해 추가 조회를 발생시키는 것이라고 판단했기 때문이다.
A -> B 관계에서 cascade 부분을 삭제하고 다시 시도해 보았다.
예상대로 B에서 C, D를 조회하는 추가 쿼리가 날아가지 않았지만, A -> B를 조회하는 페치 조인 쿼리가 아직 남아있음을 확인하였다.
실질적인 문제 원인인 A -> B 연관관계의 쿼리를 없애주지 못햇으므로 CascadeType.ALL의 문제는 아닌듯했다.
관건은 save 시에 select 쿼리가 날아간다는 건데, 구글링 결과 아래와 같은 정보를 찾을 수 있었다.
save메서드가 호출되었을 때, JPA는 내부적으로 persist()
혹은 merge
메서드 중 하나를 선택하는데,
새로운 엔티티에 대해서는 persist()
를 호출하며, 이미 존재하는 엔티티에 대해서는 merge()
를 호출한다
본인의 로직은 이미 Id값이 존재하는 엔티티가 타깃이므로 내부적으로 merge
를 실행한다. merge
는 내부적으로 SELECT -> UPDATE 흐름의 쿼리를 실행하므로 수정된 엔티티에 대해 repository.save()
를 진행하게 되면 바로 UPDATE가 실행되는 대신 SELECT- > UPDATE가 실행되는 것이 당연한 것이었다.
이를 해결하기 위해 Dirty Checking을 사용하고자 한다.
Dirty Checking은 간략하게 말해서 엔티티의 변경사항을 자동으로 저장하는 방식이다.
JPA는 트랜잭션 내부에서 영속 상태의 엔티티의 필드이 변경되면
트랜잭션이 커밋될 때 영속성 컨텍스트에서 저장된 값과 현재 엔티티 값을 비교하여 바뀐 필드에 대해 자동으로 UPDATE가 발생시킨다.
이를 적용하기 위해, repository.save()
호출을 삭제하고, 트랜잭션을 선언하여 트랜잭션 내부에서 값이 수정되도록 Dirty Checking에 맞게 로직을 변경하였다.
@Async
@Transactional
public void summarizeExperience(Experience experience, List<SubExperience> subExperiences) {
...
Experience fresh = experienceRepository.findById(experience.getId()).orElseThrow();ㄴ
fresh.setSummary(summary); // dirty checking 대상
}
다음과 같이 엔티티를 조회하여 값을 수정하는 총 2개의 최소 쿼리만 실행됨을 확인할 수 있다.
기존에 발생하던 5개의 쿼리에서 2개의 쿼리로 불필요한 쿼리를 없앨 수 있었다.
repository.save(T entity)
는 내부적으로 persist()
혹은 merge()
를 호출한다.persist()
가 호출되고,merge()
가 호출된다.merge()
호출 시에는 SELECT + UPDATE가 발생한다.CascadeType.ALL
옵션은 save시 연관 엔티티까지 작업 범위로 판단하여 추가 조회 쿼리를 발생시킬 수 있다.save()
를 사용하기보다 트랜잭션 내부에서 필드만 수정하고 Dirty Checking을 사용하는 것이 불필요한 쿼리를 줄일 수 있다.