테스트 코드 최적화 2 : 테스트 컨텍스트 관리와 통일성

devty·2025년 3월 5일

SpringBoot

목록 보기
7/11
post-thumbnail

서론

  • 첫 번째 글에서는 DirtiesContext를 제거하고 DatabaseCleaner를 도입하여 테스트 속도를 비약적으로 개선한 사례를 소개했습니다.
  • 이번 글에서는 테스트 컨텍스트 관리와 통일성 확보에 대해 이야기하려고 합니다.

테스트 컨텍스트란?

  • 스프링 테스트는 애플리케이션 컨텍스트(ApplicationContext)를 캐싱하여 여러 테스트에서 재사용하도록 설계되어 있습니다.
  • 컨텍스트 캐싱을 통해 테스트 실행 속도를 높이고, 반복적인 컨텍스트 초기화로 인한 리소스 낭비를 방지할 수 있습니다.
  • 하지만, 테스트 환경에서 컨텍스트 재사용이 제대로 이루어지지 않는 경우 테스트 실행 시간이 길어질 뿐만 아니라, 자원의 낭비로 이어질 수 있습니다.

문제점: 컨텍스트 재사용 불일치

  • 증상
    • 테스트 실행 중 중간중간 테스트가 잠깐씩 멈추는 현상이 발생했습니다.
    • 이는 새로운 애플리케이션 컨텍스트를 로드하는 데 걸리는 시간이 누적된 결과였습니다.
  • 증상이 발생하는 테스트
    • @DataJpaTest
    • @WebMvcTest
  • 새로운 애플리케이션 컨텍스트를 생성하는 이유
    • @DataJpaTest는 JPA 관련 테스트를 위한 별도의 컨텍스트를 로드하기 때문에, 다른 테스트 환경과 설정이 다릅니다.
    • @WebMvcTest는 모든 빈을 로드하지 않고 컨트롤러와 관련된 빈만 로드하기 때문에, 테스트 환경의 설정이 조금이라도 다르면 새로운 컨텍스트를 생성하게 됩니다.
  • 예시 사진
    • 위 사진과 같이 @WebMvcTest, @DataJpaTest 클래스를 각각 3개씩 모두 다른 빈을 로드하는 경우에 각기 다른 스프링 어플리케이션이 생성되는 것을 볼 수 있습니다.
    • 총 6개의 스프링 어플리케이션이 생성 되었습니다.

Application Context 새로 로드 조건

테스트 유형새로 로드되는 조건
DataJpaTest엔티티 스캔 경로가 다를 경우, Hibernate 관련 설정이 다를 경우.
WebMvcTestMockBean의 정의가 다를 경우, 테스트 클래스의 컨트롤러 정의가 다를 경우.

리팩토링: 컨텍스트 재사용을 위한 구조 통합

  • AcceptanceTest, IntegrationTest, WebMvcTest, DataJpaTest의 통합
  • 테스트 구조를 다음과 같이 리팩토링하여 컨텍스트 재사용이 가능하도록 만들었습니다.
    1. 공통 추상 클래스 도입:
      • AcceptanceTest, IntegrationTest, WebMvcTest 각각의 테스트 공통 설정을 추상 클래스로 묶어 관리.
    2. Mock과 Config 통일:
      • 테스트 클래스 간 Mock 설정 및 Config 주입을 통일하여 새로운 컨텍스트 생성을 방지.

@DataJpaTest

  • Before
    • UserRepositoryTest
      @DataJpaTest
      @ActiveProfiles("test")
      class UserControllerTest {
      
          @MockBean
          protected UserRepository userRepository;
      
          @Test
          void 유저_생성() {
              // 테스트 로직 작성
          }
      
          @Test
          void 유저_조회() {
              // 테스트 로직 작성
          }
      }
    • PostRepositoryTest
      @DataJpaTest
      @ActiveProfiles("test")
      class PostRepositoryTest {
      
          @MockBean
          protected PostRepository postRepository;
      
          @Test
          void 게시글_생성() {
              // 테스트 로직 작성
          }
      
          @Test
          void 게시글_조회() {
              // 테스트 로직 작성
          }
      }
  • After
    • RepositoryTest
      @DataJpaTest
      @ActiveProfiles("test")
      public abstract class RepositoryTest {
      
          @Autowired
          protected UserRepository userRepository;
      
          @Autowired
          protected PostRepository postRepository;
      }
    • UserRepositoryTest
      class UserRepositoryTest extends RepositoryTest {
      
          @Test
          void 유저_저장() {
      		// 테스트 로직 작성
          }
      
          @Test
          void 유저_조회() {
      		// 테스트 로직 작성
          }
      }
    • PostRepositoryTest
      class PostRepositoryTest extends RepositoryTest {
      
          @Test
          void 게시글_저장() {
      		// 테스트 로직 작성
          }
      
          @Test
          void 게시글_조회() {
      		// 테스트 로직 작성
          }
      }

@WebMvcTest

  • Before
    • UserControllerTest
      @WebMvcTest(controllers = UserController.class)
      class UserControllerTest {
      
          @Autowired
          protected MockMvc mockMvc;
      
          @Autowired
          protected ObjectMapper objectMapper;
      
          @MockBean
          protected UserService userService;
      
          @Test
          void 유저_생성() {
              // 테스트 로직 작성
          }
      
          @Test
          void 유저_조회() {
              // 테스트 로직 작성
          }
      }
    • PostControllerTest
      @WebMvcTest(controllers = PostController.class)
      class PostControllerTest {
      
          @Autowired
          protected MockMvc mockMvc;
      
          @Autowired
          protected ObjectMapper objectMapper;
      
          @MockBean
          protected PostService postService;
      
          @Test
          void 게시글_생성() {
              // 테스트 로직 작성
          }
      
          @Test
          void 게시글_조회() {
              // 테스트 로직 작성
          }
      }
  • After
    • ControllerTest
      @WebMvcTest({
              UserController.class,
              PostController.class,
      })
      @ActiveProfiles("test")
      public abstract class ControllerTest {
      
          @Autowired
          protected MockMvc mockMvc;
      
          @Autowired
          protected ObjectMapper objectMapper;
      
          @MockBean
          protected UserService userService;
      
          @MockBean
          protected PostService postService;
      }
    • UserControllerTest
      class UserControllerTest extends ControllerTest {
      
          @Test
          void 유저_생성() {
              // 테스트 로직 작성
          }
      
          @Test
          void 유저_조회() {
              // 테스트 로직 작성
          }
      }
    • PostControllerTest
      class PostontrollerTest extends ControllerTest {
      
          @Test
          void 게시글_생성() {
              // 테스트 로직 작성
          }
      
          @Test
          void 게시글_조회() {
              // 테스트 로직 작성
          }
      }

IntelliJ 테스트 시간 표시와 실제 성능 차이

  • 테스트 최적화 작업을 통해, Application Context 재사용을 구현하고 불필요한 컨텍스트 로드를 제거했습니다.
  • 하지만 IntelliJ에서 표시된 테스트 실행 시간만으로는 최적화의 성과를 온전히 확인하기 어려운 경우가 있습니다. ⇒ IntelliJ 테스트 시간의 한계
    • IntelliJ는 테스트 실행 시간만을 측정하며, 애플리케이션 컨텍스트(Application Context)를 새로 로드하는 시간은 누적하지 않습니다.
    • 따라서, 중간중간 발생하는 컨텍스트 로드 시간은 테스트 실행 시간에 포함되지 않기 때문에, 겉으로 보이는 시간은 큰 차이가 없는 것처럼 보일 수 있습니다.

실제 성능 차이

  • 실제로 컨텍스트 로드 시간은 테스트 성능에 큰 영향을 미칩니다.
    • 컨텍스트를 로드하는 데 소요되는 시간은 몇 초에서 수십 초에 달할 수 있습니다.
    • 테스트 실행 중 여러 번 컨텍스트를 새로 로드하면 누적 시간이 크게 증가합니다.
  • 최적화를 통해 동일한 애플리케이션 컨텍스트를 재사용하도록 수정한 후, 실제로는 테스트 시간이 크게 단축되었습니다.

결론

  • IntelliJ에서 측정된 테스트 시간은 최적화 전후로 차이가 크지 않을 수 있지만, 이는 테스트 과정의 실제 성능 향상을 반영하지 못합니다.
  • 최적화 작업의 성과를 검증하려면, 전체 테스트를 실행하면서 컨텍스트 로드 횟수를 직접 확인하거나, 전체 실행 시간을 측정해 비교하는 것이 중요합니다.
  • 최적화 후에는 중간중간 멈추던 컨텍스트 로드 지점이 사라지면서 테스트가 훨씬 매끄럽게 실행되는 것을 체감할 수 있습니다.
profile
지나가는 개발자

0개의 댓글