엔티티의 기본키 값을 직접 할당할 경우에 EntityManager.persist()를 호출하여 영속성 컨텍스트에서 관리하려는 순간 기본키가 있다면 merge를 수행 기본키가 없다면 persist를 호출하게 됩니다.
@Entity
public class Order extends BaseEntity {
@Id
@Column(name = "id")
private String uuid;
}
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
@Entity
public class OrderItem extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
@Entity
public abstract class Item extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
insert 쿼리는 보이지 않지만 테스트 로그에 Rolled back 이라는 문구를 확인할 수 있습니다. Test 코드에서의 @Transactional은 메서드가 끝나면 commit이 아니라 rollback을 수행하게 되어 있습니다.
insert 쿼리를 보지 못했던 이유
실습에서 기본키 매핑 전략이 직접할당 방식과 AUTO를 사용한 점이 있습니다.
현재 Order를 save하는 과정인데 Order는 기본키가 직접 할당되어 있기 때문에 먼저 select 쿼리를 실행하고 마지막에 insert 쿼리는 보이지 않습니다.
그리고 Order와 연관관계가 맺어진 Member, OrderItem, Item같은 경우는 자동생성 전략을 사용하고 있으며 그 중에서 AUTO 전략을 사용하고 있습니다. AUTO전략은 데이터베이스의 방언을 따르게 되기 때문에 실습에서는 H2 데이터베이스를 사용하며 H2 데이터베이스는 SEQUENCE 전략을 따르게 됩니다. 따라서 위의 사진의 초록박스를 보게되면 총 3개의 시퀀스를 조회하는 문장을 확인하실 수 있습니다.
만약 전략이 IDENTITY을 수행했다면 IDENTITY 전략은 SEQUENCE 전략과 다르게 persist 시점에 insert 쿼리를 수행하게 됩니다. 즉 트랜잭션 커밋 시점에 쿼리가 실행되는게 아니기 때문에 전략이 IDENTITY였다면 테스트코드에서도 insert 쿼리를 수행하는 로그를 확인할 수 있습니다.
테스트코드의 @Trasantional이 RollBack을 수행한다는 점을 자세히 알지 못했습니다.
테스트 메서드에 @Transactional을 붙이면 insert 쿼리가 나오지 않고 애노테이션을 붙이지 않으면 insert 쿼리가 찍히는 모습을 확인할 수 있었습니다. 그 이유는 트랜잭션 전파와 관련이 있습니다.
테스트 코드에 @Transactional 애노테이션을 붙이지 않으면 service에서 save를 호출할 때 트랜잭션을 수행하게 됩니다. 테스트에서 트랜잭션을 관리하지 않기 때문에 save 메서드가 끝이나면 commit을 수행하게 되면서 insert 쿼리가 찍히는 모습을 확인할 수 있습니다.
하지만 테스트 코드에 @Transactional 애노테이션을 붙이게 되면 @Trasactional 전파의 기본 설정은 REQUIRED로 상위에 부모 트랜잭션이 존재한다면 부모 트랜잭션으로 합류하게 됩니다.
즉 save가 호출되어 새로운 @Transactional 이 시작 되더라도 수행 후 부모로 합류하게 되어 save 메서드가 종료되어도 커밋을 수행하지 않습니다.
그리고 테스트 메서드가 끝나면 트랜잭션이 끝이 나는데 위에서 말씀을 드린 것처럼 테스트 코드에서의 @Transactional 애노테이션은 테스트가 끝날 때 commit이 아니라 rollback을 수행하기 때문에 insert 쿼리들이 보이지 않았던 것입니다.
deleteAll() 메소드를 수행하면 왜 쿼리를 볼 수 있었을까?
deleteAll()의 구현체를 보면 findAll() 메서드를 호출하는 것을 확인할 수 있습니다. EntityManager가 flush하는 조건 중의 하나는 JPQL을 사용해서 쿼리를 이용할때 한번 선행적으로 flush를 수행하게 됩니다.
그 이유는 영속성 컨텍스트에서 관리되어지고 있는 객체와 실제 데이터베이스와의 데이터가 다를 수 있기 때문입니다.
TypedQuery는 JPQL을 실행시키기 위한 객체입니다. 따라서 우리가 AfterEach에서 deleteAll 메서드를 추가했을때 insert 쿼리를 볼 수 있었던 이유는 이 findAll 메서드 때문이라는 것을 확인할 수 있습니다.
public void deleteAll() {
for (T element : findAll()) {
delete(element);
}
}
public List<T> findAll(@Nullable Specification<T> spec) {
return getQuery(spec, Sort.unsorted()).getResultList();
}
protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
return getQuery(spec, getDomainClass(), sort);
}
멘토님의 첨언
deleteAll 보다 deleteAllInBatch를 이용하는 것이 더 좋다고 말씀해 주셨습니다! deleteAll은 위 처럼 하나씩 지우고 있지만 deleteAllInBatch는 delete from ${table}로 한번에 지워주기 때문인 것 같습니다.
결론
테스트코드에서 insert쿼리를 확인할 수 없었던 이유를 알아보니 꽤 다양한 상황이 있었습니다. 위의 다양한 상황에 대해 저희가 한번씩 더 공부해보면 좋을만한 내용들을 간략하게 나열하면