보다 빠른 테스트를 위해 컨텍스트 캐싱하기

lango·2023년 9월 12일
3
post-thumbnail

최근 TDD를 기반으로 한 단위 테스트, 통합 테스트 등 테스트 코드를 작성하는 것에 대한 열망이 커져 다양한 자료나 문서를 찾아보고, 강의를 들으며 공부하고 있다.

그런데, 사이드 프로젝트의 계층별로 테스트 코드를 작성한 후, 테스트를 실행해보니 스프링 컨텍스트가 여러 번 실행되고 있었다. 이를 통해 컨텍스트가 새로 띄워질 때마다, 즉 서버 실행 횟수가 많아질수록 테스트 속도의 차이가 나게 된다는 것을 알게 되었다.

그래서 어떻게 서버 실행 횟수를 줄일 수 있을까? 라는 고민을 통해 테스트가 실행되는 환경을 통합하여 컨텍스트 캐싱을 통해 테스트의 속도를 더 빠르게 개선할 수 있었다.

이번 글에서는 이 과정을 간단히 기록해보려 한다.




👀 스프링 컨텍스트의 실행 횟수는?

먼저 작성한 테스트 코드의 구성을 간단히 살펴보자.

  • Presentation Layer(Controller): @WebMvcTest를 이용해 웹 계층 테스트에 필요한 빈만 주입받아 슬라이스 테스트하는 방식으로 작성했다.
  • Business Layer(Service): @SpringBootTest를 이용해 비즈니스 로직을 위한 모든 빈을 실제로 주입받는 통합 테스트 방식으로 작성했다.
  • Persistence Layer(Repository): @DataJpaTest를 이용해 JPA 설정에 필요한 빈만 주입받아 슬라이스 테스트하는 방식으로 작성했다.

다음으로 전체 테스트를 실행한 후, 컨텍스트의 실행 로그만 간단히 추렸다.

위에서 볼 수 있듯이, 사이드 프로젝트의 전체 테스트를 실행했을 때, 스프링 컨텍스트가 실행되는 횟수는 ControllerTest 2번, ServiceTest 1번, RepositoryTest 1번으로 총 4번 실행되었다.




🤔 컨텍스트가 왜 새로 실행되는 걸까?

테스트 실행 로그를 보고 왜 컨텍스트가 여러 번 실행되는 것인지 원인을 분석해보았다.

Q1. 계층별로 실행되는 걸까? ❌

ControllerTest마다 새로운 컨텍스트를 띄우는 것을 직접 보았다. 이를 통해 계층별로 컨텍스트가 구분되어 실행된다는 추측은 틀린 것 같다.

Q2. 테스트 시 같은 프로퍼티를 설정했다면? ❌

테스트마다 @ActiveProfiles("test") 어노테이션 설정을 통해 application-test.yml 설정 파일을 바라보도록 하였다.

@ActiveProfiles("test")이 설정된 테스트 클래스 중에서 해당 설정을 제거한 후 테스트를 실행해보면, 위와 같이 컨텍스트가 새로 실행되어 4번에서 5번으로 증가한 것을 볼 수 있다. 기본값으로 설정되는 프로퍼티와는 다른 컨텍스트가 실행된다는 것을 알 수 있었다.

하지만, ServiceTest와 RepositoryTest는 여전히 같은 프로퍼티가 설정되어 있음에도 서로 다른 컨텍스트에서 테스트가 수행되고 있기에 이번 추측 또한 틀린 것 같다.

Q3. 설정된 어노테이션별로 다르게 실행되는 걸까? ✅

그렇다면 서로 다른 컨텍스트가 실행된 테스트 클래스의 차이점으로 설정된 어노테이션 여부를 생각해볼 수 있다. 또한, 계층별로 슬라이스 테스트, 통합 테스트로 구분했기에 다른 컨텍스트가 실행되는 것이 당연한 수순일 것으로 느껴졌다.

앞에서 설명했지만, 한 번 더 계층별 테스트 구성을 살펴보자.

Presentation Layer(Controller)

@WebMvcTest(controllers = xxxController.class)
class xxxControllerTest { ... }

Business Layer(Service)

@SpringBootTest
class xxxServiceTest { ... }

Persistence Layer(Repository)

@DataJpaTest
class xxxRepositoryTest { ... }

여기서 주목해야 할 어노테이션은 @WebMvcTest, @SpringBootTest, @DataJpaTest 세 가지이다.

이 어노테이션들을 자세히 보니 모두 공통으로 @ExtendWith(SpringExtension.class) 어노테이션을 사용하고 있으며, @BootstrapWith 어노테이션 설정의 차이가 있음을 찾아냈다.

💡 @BootstrapWith 어노테이션은 스프링 프레임워크의 테스트 컨텍스트를 시작할 때 특정한 커스텀한 TestContextBootstrapper를 지정하기 위한 어노테이션이다.

@WebMvcTest, @SpringBootTest, @DataJpaTest 세 어노테이션은 @BootstrapWith 어노테이션 설정이 다르기에 서로 다른 스프링 컨텍스트를 생성하여 사용하게 된다는 것으로 이해했다.

결국, 테스트 클래스에 설정된 테스트 환경(어노테이션 등)에 따라 스프링 컨텍스트가 분리되어 실행된다는 것을 알게 되었다.




🛠️ 테스트 환경을 통합하여 컨텍스트 캐싱하기

컨텍스트 비용을 굳이 줄여야 해?

테스트 환경을 통합하기에 앞서, 스프링 컨텍스트가 실행되는 횟수를 줄이는 것이 과연 좋은걸까? 라는 질문을 먼저 던지고 싶다. 지금은 끽해야 4번 정도 실행되는 소규모의 프로젝트이기에 크게 체감이 안되겠지만, 프로젝트나 서비스 규모가 확장됨에 따라 컨텍스트 실행 비용은 무시 못 할 정도일 것이다.

그렇기에 테스트 수행 환경을 일관성 있게 통합하여 같은 컨텍스트를 재활용하여 테스트를 수행하도록 개선한다면, 테스트 속도도 빨라질 테고 이로 인한 생산성 및 유지보수성 향상을 기대해볼 수 있을 것이다!

동일한 테스트 환경을 가지도록 설정하기

각각의 테스트 클래스마다 어노테이션을 일일이 설정해주었는데, 계층별로 필요한 어노테이션이 지정된 테스트 환경을 제공하도록 테스트 코드를 변경하였다.

Presentation Layer만을 위한 테스트 환경

기존에는 ControllerTest 수행 시 클래스 개수마다 컨텍스트가 띄워져 가장 비용이 큰 상황이었다. 그래서 어떤 컨트롤러 테스트를 수행해도 하나의 컨텍스트를 재사용하도록 아래와 같은 ControllerTestSupport라는 추상 클래스를 정의했다.

@WebMvcTest(controllers = {
        AController.class,
        BController.class,
        ...
})
public abstract class ControllerTestSupport { ... }

이후, 위와 같이 @WebMvcTest 어노테이션에 controllers 옵션에 수행할 컨트롤러 클래스를 모두 명시해주면 된다.

// AControllerTest
class AControllerTest extends ControllerTestSupport { ... }

// BControllerTest
class BControllerTest extends ControllerTestSupport { ... }

그리고 각각의 ControllerTest에 설정된 어노테이션들을 제거한 후 ControllerTestSupport를 상속받도록 하면, ControllerTest마다 일일이 어노테이션을 설정해 테스트 환경을 설정할 필요 없이 하나의 추상클래스라는 테스트 환경만을 관리하면 된다.

테스트 코드를 수정한 후 전체 테스트를 수행하니 여러 ContollerTest에 필요한 컨텍스트를 재사용되어 실행 횟수가 1번 줄어들어 총 3번이 되었다.

Business Layer와 Persistence Layer을 아우르는 테스트 환경

기존의 ServiceTest는 컨텍스트 실행시 @SpringBootTest만을 명시해주면 같은 컨텍스트를 사용할 수는 있다. 그리고 RepositoryTest도 마참가지로 @DataJpaTest만을 명시해주면 같은 컨텍스트를 사용한다.

여기서, RepositoryTest의 경우 꼭 @DataJpaTest를 사용할 필요가 있을까? 라는 생각을 해볼 수 있다. 물론 @DataJpaTest를 사용하면 슬라이스 테스트를 통해 JPA와 관련된 테스트 설정만을 로드할 수 있다는 장점이 있기는 하나, 전체 테스트를 수행한다는 관점에서 바라보면 컨텍스트가 추가로 실행되기 때문에 속도 측면에서는 좋지 않을 수 있다는 생각이 들었다.

그래서 ServiceTest와 RepositoryTest에게 통합 테스트를 진행하는 Business Layer를 기준으로 동일한 테스트 환경을 부여하여 조금 더 빠른 테스트를 수행할 수 있도록 변경하였다.

Presentation Layer와 마찬가지로 Business Layer 및 Persistence Layer의 테스트 환경이 정의된 IntegrationTestSupport라는 추상 클래스를 만들었다.

@SpringBootTest
@ActiveProfiles("test")
public abstract class IntegrationTestSupport { ... }

그리고 @SpringBootTest을 명시해주고, 같은 프로퍼티를 바라볼 수 있도록 @ActiveProfiles("test") 설정을 추가해준다.

// AServiceTest
class AServiceTest extends IntegrationTestSupport { ... }

// ARepositoryTest
@Transactional
class ARepositoryTest extends IntegrationTestSupport { ... }

마지막으로 ServiceTest와 RepositoryTest에 설정된 어노테이션들을 제거한 후 IntegrationTestSupport 상속받을 수 있도록 수정한다. 이로 인해, ControllerTest와 마찬가지로 ServiceTest와 RepositoryTest의 어노테이션 중복 설정을 줄이고, 통합 테스트 환경을 하나의 추상클래스로 관리할 수 있게 된다.

@SpringBootTest를 사용한다면, @DataJpaTest에서 자동으로 제공해주는 @Transactional이 적용되지 않기에 RepositoryTest에 @Transactional 어노테이션을 추가로 설정해야 한다.

위와 같이 테스트 코드를 변경하고 테스트를 수행하니 ServiceTest의 컨텍스트를 RepositoryTest에서 재사용하게 되어 실행 횟수가 1번 더 줄어들어 총 2번이 되었다.




✨ 더 고민할 점은?

지금까지 Presentation Layer에 대한 테스트 환경, 그리고 Business Layer와 Persistence Layer의 통합 테스트 환경을 구성하여 4번 실행되던 스프링 컨텍스트를 2번까지 줄일 수 있었다.

그리고 테스트에 Mock을 사용하게 된다면 추가로 고려해야 할 사항이 있다. 테스트 코드를 작성하면서 이번 주제와 유사한 이유로 성능 향상을 위해 Mock을 사용하게 되는데, 여기서 @MockBean을 통해 Mock 객체를 주입했을 때의 컨텍스트 환경과 Mock 객체가 없는 컨텍스트 환경이 분리되는 상황도 발생한다.

그래서 프로젝트의 상황을 고려하여 Mock 처리가 필요 없는 테스트 환경을 위한 순수한 TestSupport 상위 클래스와, Mock 처리가 필요한 테스트 환경을 위한 MockTestSupport 상위 클래스를 별도로 구축하여 Mock을 기준으로 테스트 환경을 구분하는 것도 좋은 솔루션이 될 수 있다.




🚩 마치며

사실 테스트 환경을 구성하는 데에는 정답이 없다고 생각한다. 이번 글에서는 전체 테스트 수행 속도를 줄이기 위해 테스트 환경을 통합한 것이지만, 통합 테스트를 수행하는 것보다 단위 테스트를 수행하는 빈도수가 많은 상황에서는 단위 테스트의 속도가 느려 적절한 솔루션이 되지 못할 수도 있기 때문이다. 그래서 상황에 따른 Trade-off를 잘 따져야 한다.

그리고 상황에 맞춰 정답에 가까운 적절한 솔루션을 제시하려면, TDD를 비롯한 테스트에 대한 기술적 지식의 필요성을 느꼈다. 그뿐만 아니라 테스트를 위한 환경을 어떻게 제공할 것이냐에 따른 아키텍처 시점에서의 지식도 무시할 수 없다.

마지막으로 테스트 공부 전/후로 테스트 코드 작성 능력뿐만 아니라, Java와 Spring의 다양한 기술적 요소도 배우고 파헤쳐볼 수 있었고, 막연했던 테스트 작성에 대한 안목을 늘릴 수 있었고, 테스트 코드의 기술적 깊이는 부족하겠지만, 어떻게 테스트 코드를 작성해야할지 정도의 수준으로 성장했다고 생각한다!




🙏 개인적이고 주관적인 관점에서 작성된 글입니다. 혹시라도 잘못된 정보나 누락된 정보가 있을 경우 지적해주시면 다시 공부하겠습니다.


또한, 이번 글에서 소개한 내용은 박우빈님의 강의를 참고했으며, 사용한 코드 관련 내용은 Github에서 확인하실 수 있습니다.


참고 자료

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글