이전에 프로젝트를 진행할 때는, 구현 코드를 작성하고 테스트 코드를 작성하지 않았었습니다.
하지만 이번 프로젝트에서는 Controller
, Service
, Repository
, Entity
모든 계층에서 단위 테스트를 진행하였습니다.
그리고 단위 테스트 코드를 작성하면서 단위 테스트의 중요성, 느낀점 및 배운점에 대해서 정리해보려고한다.
숙소 검색 API에 대한 성능 테스트를 하기 위해서 대용량 데이터를 삽입하는 과정에서 , detached entity passed to persist
에러를 겪었습니다.
에러의 원인은 이미 생성되어 있는 객체의 부분에 다시 한번 더 저장하려고 했기 때문입니다.
즉, 엔티티 종속성 문제였습니다.
검색 시 예약에 대한 조건이 있기 때문에, 예약 데이터도 추가했어야했습니다.
즉, 예약 데이터는 유저와 객실 데이터가 존재할 때 의미가 있기 때문에, 예약을 생성하기 위해서는 유저와 객실이 존재해야합니다.
public void createReservationData(int idx) {
IntStream.rangeClosed(1, idx).forEach(e -> {
User user = userDao.findById(rnd.nextLong(1, 10)).get();
Room room = roomDao.findById(rnd.nextLong(1, 2000)).get();
LocalDate startAt = getRandomDateBetween(LocalDate.of(2024, 4, 10), LocalDate.of(2024, 4, 25));
LocalDate endAt = startAt.plusDays(2);
Reservation saveReservation = createReservation(user, room, startAt, endAt);
reservationDao.save(saveReservation); // 에러 발생 지점
reservations.add(saveReservation);
});
}
그렇기 때문에, 예약을 저장하는 코드에서 에러가 발생하게됩니다.
왜냐하면, 예약을 저장할 때 유저와 객실은 findById
메서드를 통해 이미 존재하는 데이터이기 때문입니다.
두 가지 해결방법을 찾았습니다.
CascadeType.PERSIST
삭제@Transactional
사용어떤 방법을 선택할 지, 예약 도메인을 담당하신 팀원분과 이야기를 진행했습니다. 실제로 예약 데이터는 유저와 객실 데이터가 존재해야하기 때문에 CascadeType.PERSIST를 삭제하는 방법을 제안했습니다.
하지만, Service 계층에서 같은 Transaction에 있다면 문제가 발생하지 않기 때문에 큰 문제는 없을 것 같다고 말씀해주셨고, 구현 코드에서도 큰 문제는 발생하지 않았기 때문에 CascadeType.PERSIST 속성을 제거하는 방법이 아니라 @Transactional을 사용하여 JPA 영속성 컨텍스트가 테스트 코드 단위로 유지되도록 하여 문제를 해결했습니다.
이번 에러를 통해서 JPA 영속성 컨텍스트가 Transaction 단위로 유지된다는 것을 다시 한번 알게 되었으며, 영속성 컨텍스트 관리에 유의해서 코드를 작성해야겠다 느꼈습니다.
테스트 코드 작성 없이 진행한 프로젝트에대해서 리팩토링을 진행할 때는 두려움이 있었습니다.
왜냐하면, 코드의 변경으로 에러를 만나게 되면 병목지점이 어디인지 파악하기 어려웠기 때문입니다.
그 뿐만 아니라, 테스트 코드가 없는 상태에서 리팩토링을 수행하면 잘못된 리팩토링으로 이어질 수 있습니다.
이러한 악순환은 이어지고 프로젝트의 규모가 커지면 점점 유지보수하기 어려워지는 부정적인 결과로 이어지게 됩니다.
하지만 테스트 코드를 작성하기 시작하면서 이전보다 리팩토링하는 것을 두려워하지 않는 모습을 찾아볼 수 있었고, 리팩토링 후 오류가 발생하더라도 병목 지점을 쉽게 파악할 수 있어서 자신감이 생기기 시작했습니다.
또 한, 지속적으로 테스트 코드를 작성하면서 이전 테스트 코드의 문제점을 확인할 수 있었습니다.
모든 계층에서 테스트 코드를 작성하게 되면 엔티티 연관관계를 설정하는 로직을 반복하게 됩니다.
이를 통해 기능 단위로 테스트 코드를 작성하면서 엔티티 연관관계에 대해서 다시 한번 고민하게 되면서 이해도를 높일 수 있었습니다.
이로서 해당 로직의 오류를 빠르게 발견할 수 있었거, 수정할 수 있었습니다.
단위 테스트를 처음 작성할 때 모든 계층에서 @SpringBootTest
을 사용해서 작성했습니다.
하지만, 팀원들의 코드를 분석하면서 @DataJpaTest
를 알게 되었습니다.
@DataJpaTest
는 @SpringBootTest
와 달리 JPA 컴포넌트들만 테스트하기 위한 어노테이션으로, full-auto config를 해제하고 JPA 테스트와 연관된 config만 적용합니다.
@Target(value=TYPE)
@Retention(value=RUNTIME)
@Documented
@Inherited @BootstrapWith(value=org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper.class)
@ExtendWith(value=org.springframework.test.context.junit.jupiter.SpringExtension.class)
@OverrideAutoConfiguration(enabled=false)
@TypeExcludeFilters(value=DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest
위 코드는 @DataJpaTest
가 가지고 있는 어노테이션입니다.
몇 가지 어노테이션을 살펴보면,
@Transactional
@AutoConfigureTestDatabase
replace=AutoConfigureTestDatabase.Replace
가 디폴트로 설정되어 있어, application.yaml파일에 설정해놓은 DB가 아닌 In-MemoryDB를 활용해서 테스트가 실행됩니다.EmbeddedDatabaseConnection
클래스를 보면, H2
, DERBY
, HSQL
, HSQLDB
중 사용 가능한 In-Memory DB에 자동으로 커넥션을 설정하는 것을 확인할 수 있습니다.이처럼 @DataJpaTest
를 사용하면 자동으로 Embedded DB를 사용하며, 자동으로 롤백해준다.
만약 EmbeddedDB가 아니라 실제 DB를 사용하고 싶을 때는, @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
를 사용해서 application.yaml 파일에 설정된 DB로 연결할 수 있습니다.
그렇다면, @SpringBootTest
어노테이션들을 살펴보자
@Target(value=TYPE)
@Retention(value=RUNTIME)
@Documented
@Inherited
@BootstrapWith(value=SpringBootTestContextBootstrapper.class)
@ExtendWith(value=org.springframework.test.context.junit.jupiter.SpringExtension.class)
public @interface SpringBootTest
코드를 살펴보면@ExtendWith(value=org.springframework.test.context.junit.jupiter.SpringExtension.class)
과 같이 전체 SpringBoot 애플리케이션 컨텍스트를 로드하며, 실제 애플리케이션과 거의 동일한 환경에서 테스트를 수행할 수 있도록 모든 빈과 설정을 포함합니다.
따라서, @SpringBootTest
는 모든 빈을 로드하기 때문에 무겁고 시간이 많이 걸리고, @DataJpaTest
는 필요한 JPA 관련 빈들만 로드하기 때문에 더 가볍고 빠르게 실행됩니다.
만약 JPA와 관련된 테스트를 진행하기 위해서는 @SpringBootTest
를 사용하는 것은 모든 Bean들을 Application Context에 로딩하기 때문에 @DataJpaTest
를 사용하여 시간 비용을 줄일수 있다는 것을 알게 되었습니다.
단위 테스트를 알기 전에도 주변에서 '단위 테스트는 중요하다'라는 소리를 많이 들었었지만, 저에게 와닿지가 않았었습니다.
돌이켜 이유를 생각해보면, 이전에 프로젝트는 회원
, 게시글
, 댓글
테이블만 가지는 소규모 프로젝트를 진행했었기 때문이라고 생각합니다.
하지만, 이번 프로젝트는 회원
, 숙소
, 숙소 시설
, 숙소 이미지
, 객실
, 객실 이미지
, 리뷰
, 예약
등 이전에 진행했던 프로젝트보다 규모가 큰 팀 프로젝트였습니다.
그럼에도 불구하고, 모든 계층에서 테스트 코드를 작성함으로 써 gradle 테스트를 진행하여 병목 지점을 쉽게 찾을 수 있는 것을 경험했습니다.