변경 사항이 있으면 쓰기 지연 sql 저장소에 update 쿼리를 생성하고, flush를 통해 db에 반영한다.
flush : 쓰기 지연 sql 저장소에 있는 sql을 실행하여 db에 쿼리를 날리는 행위, 1차캐시 삭제x
2. 엔티티의 생명주기
비영속(new/transient)
객체를 생성만 한 상태
영속(managed)
객체를 생성한 상태 + 객체를 저장(persist)한 상태
DB에 쿼리를 날리지 않음 -> 트랜잭션이 commit을 해야 쿼리가 나감
준영속(detached)
영속성 컨텍스트가 관리하지 않음
변경감지x
detach()
1차 캐시에 해당 객체 지우기 -> 쓰기지연 sql 저장소 지우기
삭제(removed)
3. 테스트
3.1 save() 메소드 관련
@SpringBootTest
class MemberRepositoryTest {
//...
private static final Member 홍길동 = new Member("홍길동");
@DisplayName("저장_후_비교_한다")
@Test
void 저장_후_비교_한다() {
log.info("홍길동이 비영속 상태 인가요? {}", entityInformation.isNew(홍길동));
log.info("홍길동 ID = {}", 홍길동.getId());
Member 결과 = memberRepository.save(홍길동);
assertThat(결과).isEqualTo(홍길동);
}
@DisplayName("회원_찾기")
@Test
void 찾기() {
log.info("홍길동이 비영속 상태 인가요? {}", entityInformation.isNew(홍길동));
log.info("홍길동 ID = {}", 홍길동.getId());
Member 저장됨 = memberRepository.save(홍길동);
Optional<Member> 결과 = memberRepository.findById(저장됨.getId());
assertThat(결과).hasValue(홍길동);
}
//...
}
테스트 두 개를 같이 돌리면 실패하게 된다.
테스트 격리가 안되서 발생한 문제도 있지만, 추가로 영속성과 관련된 문제도 존재한다.
static으로 선언된 홍길동이 먼저 영속 상태가 되고, 그 객체를 다른 테스트에서도 사용해서 문제가 발생한다.
이 문제를 해결할 방법을 찾으려면 save() 메서드에 대한 이해가 필요하다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
//...
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
//...
}
save() 메서드를 보면 isNew()메서드로 영속상태인지 확인하고, 비영속상태이면 persist()를 실행하고, 아니면 merge()를 실행한다.
persist()는 null인 Id를 새로 발급하는 과정이고, merge()는 기존 아이디를 가지고 있어도 복사본을 만들어 새로운 Id로 발급하는 과정이다. 이후에는 모두 영속상태가 된다.
주의할 점은 merge()는 객체를 교환하는 형식이기 때문에 일부분만 변경을 시도하면, 변경이 안된 부분이 null 값으로 들어갈 위험이 있다.
따라서 테스트를 실행하면 persist와 merge가 둘 다 발생해서 동일성이 보장이 안된다.
그러므로 동일성을 보장하기 위해서, 객체를 일일이 선언해서 쓰는게 더 낫다.
3.2 deleteAll() 메소드 관련
name을 unique로 설정하고 테스트를 진행한다.
deleteAndSave() 메서드는 먼저 deleteAll()을 실행하고, saveAll(List members)을 실행한다.
@DisplayName("삭제 후 저장 테스트")
@Test
void 삭제_후_저장_테스트() {
//given
memberRepository.save(new Member("홍길동"));
memberRepository.save(new Member("김철수"));
//when
//then
assertThatThrownBy(() -> memberService.deleteAndSave(List.of(
new Member("홍길동"),
new Member("김철수"),
new Member("이영희")
))).doesNotThrowAnyException();
}
}
위 테스트를 실행하면 log는 다음과 같이 나오며, UniqueViolation이 발생하며 실패한다.
왜냐하면 JPA에서는, 우리의 생각처럼 DB에 있는 내용을 먼저 지우고, 바로 저장하지 않는다.
deleteAll() 먼저 db에서 값들을 모두 찾아 1차 캐시에 저장하고, 쓰기지연 sql 저장소에 delete 쿼리를 저장하고 flush()가 발생하면 그 때 실행된다.
여기에서 위 문제가 발생하는데, 쓰기지연 저장소에 쿼리들이 존재하지만 아직 실행이 되지 않은 채로 save 메서드를 호출했기 때문에 문제가 발생했다.
그렇다면 이 문제는 어떻게 해결 할 수 있을까?
유튜브 영상에서는 아래와 같은 방법을 사용한다.
save()를 하기전에 flush()를 사용한다.
JPQL을 사용할 때 flush가 사용된다.
직접 flush()를 사용한다.
commit을 활용한다.
하지만 위 방법보다 좋은 방법이 존재한다.
deleteAllInBatch() 메서드를 사용하는 것이다.
< deleteAllInBatch() >
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java.lang.Iterable)
*/
@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {
Assert.notNull(entities, "Entities must not be null!");
if (!entities.iterator().hasNext()) {
return;
}
applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
.executeUpdate();
}
deleteAll()은 쓰기지연 때문에 문제가 발생하며, 또한 iterator로 delete() 메서드를 호출하기 때문에 삭제할 객체 수대로 쿼리가 만들어서 보내진다.
하지만 deleteAllInBatch() 위의 내용과 같이 applyAndBind를 통해 쿼리를 만들고 executeUpdate() 형식으로 쿼리를 직접 실행하므로, 쓰기 지연 문제도 해결되며, 다수의 객체를 삭제해도 쿼리를 한 개만 삭제하므로 언급했던 문제는 물론, 성능 문제도 해결된다.