Spring 컨텍스트 캐싱을 이용한 411개의 테스트 코드 최적화 과정

yeong-min·2024년 9월 28일
0
post-thumbnail

1. 문제 상황

1.1 문제점

현재 깃터디의 테스트 코드가 411개나 되어 프로그램의 안정성이 좋지만 단점이 있다. 프론트에서 사소한 오류 수정을 받아도 대량의 테스트 코드를 수정해야하는 문제가 생겼다. 이에 따라 전체 테스트 코드를 계속 돌려보는 상황이 생기는데 411개를 한번에 돌려볼 경우에 대략 50초정도가 걸리게 되어 시간적으로 손해가 컸다.

411개의 테스트 코드의 시간을 줄여보자!

1.2 문제 분석

실제 테스트 버튼을 누르고 종료될 때까지의 시간은 50초인데 위의 사진에는 13초라고 뜬다. 이 13초는 테스트 자체의 시간이며 테스트 컨텍스트를 로딩하는 시간이 수행 시간에서 제외된다.

컨텍스트 로드 시간 = 50 - 13 = 37!

새로운 컨텍스트를 로드하는 것에 37초나 걸린다.

컨텍스트 로드하는 시간을 줄여봐야겠다.


2. 문제 해결 과정

2.1 Spring Context 초기화 횟수 확인

개선 전에 ApplicationContextInitListener를 상속 받아 context가 몇 번 초기화 되는지 확인해보자.

  • 아래의 리스너를 통해 context 초기화를 카운트 해준 후 출력 해준다.
@Component
public class ApplicationContextInitListener implements ApplicationListener<ContextRefreshedEvent> {
    private static final AtomicInteger contextInitCount = new AtomicInteger(0);

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        contextInitCount.incrementAndGet();
        System.out.println("context init count : "+ contextInitCount.get());
    }
}

테스트를 실행해보니 총 23개의 context가 초기화되었다. 컨텍스트 초기화 개수를 많이 줄일 수록 테스트 시간은 비약적으로 줄어들 것 같다.


2.2 스프링 컨텍스트 캐싱 이해하기

1. 스프링 컨텍스트 캐싱 (Context Caching)

  • 스프링에서는 Context Caching이라는 최적화 기법을 제공하여, 동일한 애플리케이션 컨텍스트 구성을 재사용함으로써 테스트 시간을 줄인다.

이 기법을 이해하기 위해 스프링 테스트 실행 방식을 먼저 살펴보자

@SpringBootTest 어노테이션이 붙은 테스트를 실행하면, 해당 테스트 클래스는 실제 스프링 컨텍스트를 띄우고, 빈으로 등록된 모든 객체를 가져온다. 그러나 스프링 컨텍스트를 띄우는 작업은 시간이 오래 걸리기 때문에, 여러 테스트 클래스에서 매번 새로운 스프링 컨텍스트를 생성한다면 전체 테스트 시간이 길어질 수 있다.

이를 해결하기 위해 스프링은 동일한 컨텍스트 구성을 가진 테스트에서는 캐싱된 컨텍스트를 재사용할 수 있도록 최적화한다. 즉, 컨텍스트 구성이 동일하다면 한 번 띄운 컨텍스트를 여러 테스트에서 재사용하는 방식!

  • 스프링 공식문서에 Spring Context Caching 관련 설명이 있다.

TestClassA, TestClassB의 설정 파일이 같으면 동일한 ApplicationContext를 공유한다. 즉, 애플리케이션 컨텍스트를 로드하는 설정 비용은 테스트 스위트당 한 번만 발생하며, 이후 테스트 실행 속도는 훨씬 빨라진다고 한다.

2. @MockBean과 컨텍스트 캐싱

컨텍스트 캐싱에서 중요한 점은 테스트 클래스마다 사용된 @MockBean의 조합에 따라 컨텍스트가 달라진다는 것이다. 만약 각 테스트 클래스에서 서로 다른 빈을 Mocking하면, 스프링은 캐싱된 컨텍스트를 재사용하지 않고 새로운 컨텍스트를 로드하게 된다. 이는 @MockBean을 통해 빈 구성이 변경되었기 때문이다.

정리하면, Mocking한 빈 조합이 동일한 경우에는 캐싱된 컨텍스트를 재활용하지만, 조합이 다를 경우 새로운 컨텍스트를 띄워야 하기 때문에 컨텍스트 로딩 시간이 증가할 수 있다.

  • 스프링 공식문서

컨텍스트 구성(configuration parameters)이 달라지면 다른 컨텍스트 키가 되면서 ApplicationContext가 달라져 Spring Context Caching 기능을 사용하지 못한다.


2.3 해결 과정

  1. @MockBean은 새로 로드될 때마다 컨텍스트를 새로 실행하므로 비용이 매우 크다.

    → MockBean을 하나의 설정 클래스 안에 묶기

    // **MockBean을 모아둔 설정 파일**
    // - 이를 통해 MockBean을 만나면 컨텍스트를 재생성하는 것이 아닌 원래 로드된 MockBean을 재사용
    
    @ExtendWith(SpringExtension.class)
    @TestConfiguration
    public class CommonTestConfig {
    
        @MockBean
        private AuthService authService;
    
        @MockBean
        private StudyInfoService studyInfoService;
    
        @MockBean
        private RankingService rankingService;
    
        @MockBean
        private StudyBookmarkService studyBookmarkService;
    
        @MockBean
        private CategoryService categoryService
    }
    
    • 설정파일에 @MockBean을 몰아 넣고 MockTestConfig를 상속 받는 ControllerTest 클래스들에서는 @Autowired로 MockTestConfig 파일에 있는 목 객체를 주입받을 수 있다.
    // 변경 전
    public class StudyTodoControllerTest extends MockTestConfig {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @MockBean
        private StudyTodoService studyTodoService;
    
        @MockBean
        private StudyMemberService studyMemberService;
    }
    
    // **변경 후**
    public class StudyTodoControllerTest extends MockTestConfig {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Autowired
        private StudyTodoService studyTodoService;
    
        @Autowired
        private StudyMemberService studyMemberService;
    }
  2. @DirtiesContext 제거 하기

    예전에 컨텍스트 오염 문제로 테스트가 성공했다가 실패했다가 반복한 적이 있어서 컨텍스트 격리를 위해 @DirtiesContext를 추가해 놓은 적이 있다.

    @DirtiesContext
    - 테스트 수행 전,, 각 테스트 케이스를 수행하기 전, 후에 Context를 재생성
    - 컨텍스트가 변경되었을 때 기존 애플리케이션 컨텍스트를 폐기 -> 재생성
    - 테스트 격리를 제공
    
    왜 @DirtiesContext가 필요한가?
    - 각 테스트가 독립적으로 실행되고, 서로의 상태에 영향을 주지 않도록 보장
    - 테스트마다 서로 다른 Mock 설정을 사용하는 경우 충돌을 방지
    - 컨텍스트의 상태와 리소스를 관리하고 정리하여 일관된 테스트 환경을 유지

하지만 이제는 하나의 설정 파일안에 MockBean을 모아두고 상속 받는 식으로 구현되어있어서 처음에만 초기화하기 때문에 Mock 설정이 충돌할 일이 없다.

그래서 DirtiesContext 제거가 가능하다.

  • 스프링 공식문서 ApplicationContext 종료 조건 세가지
  1. @DirtiesContext 추가
  2. LRU(Least Recently Used) 캐시 정책
  3. JVM 종료 시점의 Shutdown Hook

    1번 때문에 ApplicationContext가 설정파일 동일 여부에 관계없이 계속 재로딩 되고 있었다.


3. 결과 및 한계

3.1 결과

1, 2번 과정을 적용한 후 테스트를 돌려보았다.

테스트 자체의 시간은 변경 전과 동일하게 13초로 비슷하다. 하지만 아까 공부했듯이 13초는 테스트 그 자체의 돌아가는 시간이고 컨텍스트 로드하는 시간에서 37초나 걸렸었다.

  • @DirtiesContext 를 제거하고 CommonTestConfig로 컨텍스트 오염을 제거해준 결과 오류 없이 411개의 테스트 코드를 50초 → 30초(40%)로 단축할 수 있었다.
  • 밑의 사진을 보면 Context Init Count도 23개 → 10개로 (57%) 줄은 것을 볼 수 있다..


3.2 한계

DirtiesContext 의 한계

DirtiesContext를 분리한 만큼 컨텍스트의 상태가 다른 테스트와 공유되면서 예상치 못한 오류가 발생할 수 있으므로 이를 방지하기 위해서는 컨텍스트의 상태를 테스트 간에 명확하게 격리해야한다.

  1. Mock 설정 분리
  2. @BeforeEach 또는 @AfterEach 어노테이션을 사용하여 각 테스트가 실행되기 전이나 후에 필요한 리소스를 초기화하거나 정리

참고 자료

https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/caching.html#page-title

0개의 댓글