Memory db를 이용한 Repository Test
Repository layer의 테스트를 위해서 내장 Memory DB를 많이 사용합니다.
Memory DB 를 사용하는 방법도 천차만별일텐데 크게는 2가지 정도라 생각됩니다.
@SpringBootTest+ Memory DB 연결@DataJpaTest
두 방법의 차이점과 [Junit & JPA의 영속성 컨텍스트]로 인한 여러 가지 현상에 대해 정확히 알기 위해 글을 작성합니다.
두 가지 방법의 쿼리 로그가 다른데?
위 2가지 방법 중 어느것을 선택해서 Repository Layer 테스트를 진행할까 고민하던 중
제 예상과는 다른 쿼리 로그가 찍히는 것을 확인했습니다.
테스트한 코드는 아래 2가지입니다.
예제 코드
// Member.java
@Getter
@Setter
@Table(name = "member")
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
}
// MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
}
@SpringBootTest 코드 & 결과
아래 코드처럼 @SpringBootTest를 이용하여 실행해보면 insert query가 실행되는 것을 확인할 수 있습니다.
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@SpringBootTest
public class TestUsingSpringBootTest {
@Autowired
private MemberRepository memberRepository;
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}

@DataJpaTest 코드 & 결과
하지만 같은코드를@DataJpaTest를 이용하여 실행하면 insert query가 실행되지 않는 것을 확인할 수 있습니다.
@DataJpaTest
class TestUsingDataJpaTest {
@Autowired
private MemberRepository memberRepository;
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}

왜 이러지? 생각보다 너무 간단한 이유였다..
이유는 생각보다 너무 간단한 이유였습니다.
아무리 간단해도 일단 JPA 영속성 컨텍스트의 동작 방식을 포함한 아래 3가지를 알고 있어야 합니다.
JPA 영속성 컨텍스트의 쿼리 실행 방식
- 영속성 컨텍스트는
flush()가 실행되기 전까지 실행될 쿼리를 가지고 있다가 한번에 쿼리를 실행하도록 설계되어있습니다. flush()가 실행되기 전까지 쿼리가 실행되지 않는다는 것은@Transactional안에서 Rollback이 되어야 하는 상황이라면 애초에 쿼리가 실행조차 되지 않을수 있다는 것입니다.
JUnit에서의 @Transactional
- JUnit을 통한 Test가 실행된다면
@Transactional이 적용되어있는 로직이 실행된다면 SELECT를 제외한 모든 쿼리는 Rollback 대상으로 취급합니다.
다시 왜 이러는지 살펴보면
위의 이론들을 알고 있는 상태에서 상황별 코드를 다시 자세히 들여다보겠습니다.
@DataJpaTest
@DataJpaTest
class TestUsingDataJpaTest {
@Autowired
private MemberRepository memberRepository;
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}
@DataJpaTest의 경우 쿼리가 Insert Query가 실행되지 않는것은@DataJpaTest를 따라가보면 바로 이해가 됩니다.

위 사진 처럼 @DataJpaTest에는 기본적으로 @Transactional이 설정되어있습니다.
그러니 @DataJpaTest로 실행한 테스트는 @Transacitonal이 자동으로 설정되게 됩니다.
@Transacitonal이 설정된 상태로 실행된 Test는 userRepository.save()로 영속성 컨텍스트에는 반영되었지만, 결정적으로 쿼리는 나가지 않는 상태가 되기 때문에 로그에도 쿼리가 보이지 않게 됩니다.
@SpringBootTest
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@SpringBootTest
public class TestUsingSpringBootTest {
@Autowired
private MemberRepository memberRepository;
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}
@SpringBootTest는 @Trasactional 없이 그대로 돌아가면서 영속성 컨텍스트의 쿼리들이 그대로 실행되면서 쿼리로그도 확인할 수 있게 됩니다.
응용하는 여러가지 실험
실험1. @SpringBootTest + @Transactional
위 내용대로라면 @SpringBootTest의 테스트 메소드에 @Transactional을 설정한다면 insert Query가 실행되지 않는 것이 예상됩니다.
아래 코드로 예상된 결과대로 동작하는것을 확인할 수 있습니다.
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@SpringBootTest
public class TestUsingSpringBootTest {
@Autowired
private MemberRepository memberRepository;
@Transactional
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}

실험2. @SpringBootTest + @Transactional + Rollback(false)
@Rollback(false)는 테스트 코드에서 자동으로 설정되는 Rollback의 실행을 강제로 설정할 수 있는 어노테이션입니다. 그렇다면 @SpringBootTest와 @Transactional을 설정한다고 해도 @Rollback(false)를 사용하게 되면 insert query의 실행이 예상이 되고 아래 코드는 예상된 결과대로 동작하는 것을 확인할 수 있습니다.
(@DataJpaTest + @Rollback(false)의 결과도 역시 동일합니다.)
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@SpringBootTest
public class TestUsingSpringBootTest {
@Autowired
private MemberRepository memberRepository;
@Rollback(false)
@Transactional
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}

GenerationType.IDENTITY의 테스트
Entity의 @GeneratedValue만 변경했는데 결과가 다르다
위 예제들에서는 기본키 채번 방식을 GenerationType.Sequence 로 테스트를 진행했었는데 GenerationType.IDENTITY 로 진행하면 또 다른 결과가 나오게 됩니다.
채번방식만 변경하여 @DataJpaTest방식의 동일한 테스트를 진행해보겠습니다.
// Member.java
@Getter
@Setter
@Table(name = "member")
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
}
@DataJpaTest
class TestUsingDataJpaTest {
@Autowired
private MemberRepository memberRepository;
@Test
void contextLoads() {
Member member = new Member();
member.setAge(10);
member.setName("fdsafdsafa");
memberRepository.save(member);
}
}

분명 @DataJpaTest는 자동으로 @Transactional이 설정되어있기 때문에 insert query가 실행되지 않아야 합니다.
그런데 GenerationType.Sequence → GenerationType.IDENTITY 로 채번 방식만 변경했을 뿐인데 insert query가 실행된 것을 확인할 수 있습니다.
다시 JPA 영속성 컨텍스트
userRepository.save() 를 이용해 영속성 컨텍스트에 담는 과정을 생각해보면 왜 insert query가 실행되었는지 알 수 있습니다.
GenerationType.Sequence- Entity를 영속성 컨텍스트에 담기 위해서는 ID를 포함한 값이 Entity가 담겨있어야 합니다.
- 결국 영속성 컨텍스트에 담기 위해서는 ID값을 채번해야된다는 뜻이되는데
GenerationType.Sequence은 별도의 쿼리를 통해 ID값만 가져오기 때문에 따로flush()가 실행되기 전까지는 따로 insert query가 실행되지는 않습니다.
GenerationType.IDENTITYGenerationType.IDENTITY는 보통 auto_increment로 동작하고 insert 하기 전에는 ID값을 알 수 없는 상태이기 때문에 영속성 컨텍스트에 담지 못합니다.- 결국 해당 Entity의 ID를 알아내기 위해
save()와 동시에 insert query를 실행하게 됩니다.
결국GenerationType.IDENTITY 가 insert query가 실행되는 이유는
결국 GenerationType.IDENTITY로 설정된 Entity의 save() 에서 실행된 insert query는 영속화 과정 중 ID 채번을 위한 insert query였습니다.
결국 Junit에서의 save() 실행 시에 Rollback 과정도 Entity별로 달라지는 것입니다.
GenerationType.IDENTITY의 Entity는 insert query 실행해서 DB상의 롤백GenerationType.Sequence는 따로 채번만 하고 insert query 실행하지 않은 상태에서 영속성 컨텍스트안에서 소멸
'Java' 카테고리의 다른 글
| 외부 API 연동 테스트 코드 @RestClientTest (2) | 2021.10.17 |
|---|---|
| 람다(Lambda)와 익명 클래스 :: Lambda can be replaced with method reference (0) | 2021.08.01 |
| 람다 캡처링 :: Variable used in lambda expression should be final or effectively final의 이유 (7) | 2021.07.14 |
| Mockito @Mock @MockBean @Spy @SpyBean 차이점 (27) | 2020.09.13 |
| @NoargsConstructor(AccessLevel.PROTECTED) 와 @Builder (5) | 2020.07.31 |