테스트코드 최적화 여행기 (1)

테스트코드 최적화 여행기 (1)

2021, Sep 30    

우테코 3단계를 진행하면서 프로젝트로 깃허브 기반의 SNS를 제작하고 있다. 팀의 컨벤션으로 테스트코드를 정말 빡시게 짜고있는데, 덕분에 리팩토링, 트러블 슈팅 등 많은 부분에서 큰 도움을 얻고 있다.

하지만 프로젝트의 기능이 하나식 추가되고 커지면서 문제점이 발생하는데, 바로 큰 도움을 준 테스트 코드가 그 주인공이다.

테스트 코드를 작성하다보니 어느 순간 500개가 넘는 TC가 쌓였다. 하지만 그에 따른 테스트 시간이 비약적으로 증가하였다. 체감상으로는 테스트가 쌓일수록 지수그래프를 따라 상승하는 것 같다.

또한 그 뿐만 아니라, 짧은 시간안에 많은 기능들을 구현하고자 하니 팀원들간의 테스트 컨벤션이 맞질 않아 테스트 픽스쳐가 여기저기 흩뿌려져 있고, 인수테스트의 경우 api를 호출하는 코드가 여기저기 중복되어 가독성 또한 심각하게 떨어지는 문제점이 있다.

이번 글에서는 이 수많은 테스트 코드를 리팩토링 하는 이야기를 담고자 한다.

현재 내가 생각하는 리팩토링 순위는 다음과 같다.

  1. 테스트 시간 단축
    1. 현재 글 작성 기준으로 전체 테스를 수행하는데 노트북(맥북 m1, 16gb)으로 약 1분이 소요되며테스크탑(인텔 10400, 16gb)의 경우는 약 3분 30초가 소요된다. 또한 ec2(medium)의 경우 는 약 4~5분이 소요된다.
    2. 따라서 리팩토링 및 프로덕션 작성 시 테스트를 돌리는데 약간은 겁이 난다. 이러한 겁은 리팩토링에 방해가 된다.. 또한 pr작성 시 젠킨스를 통해 빌드와 테스트 검사를 하고, 이를 통과 해야 merge가 가능한데, 이것만 약 5분이 소요되며 merge 후 배포시에도 다시 한번 테스트가 진행되므로 새로운 pr을 dev서버에 배포하는데만 약 10분이 소요되는꼴이다. 이걸 과연 CI라고 할 수 있을지…
  2. 공통 로직 제거
    1. 현재 너무나도 많은 중복 코드가 존재한다. 예를 들면 특정 인수테스트가 있고, 관련된 테스트가 10개라면 10개 모두에 api를 call하는 RestAssured코드가 존재하는 경우도 있다. 이러한 코드 중복을 제거하여 장황한 테스트 코드를 줄이고 가독성을 높히는게 중요하다고 생각된다.
  3. 픽스쳐 정리
    1. 픽스쳐가 여기저기 흩뿌려져 있다. 예를들어, 어느 코드에서는 유저를 생성하는데 테스트 유저를 생성하는 UserFactory를 사용하는 반면, 어느 코드에서는 new를 이용하여 테스트용 유저를 직접 생성하기도 한다. 이를 정리하여 통일성을 높히는게 중요할 것 이다.

3가지 리팩토링 순위를 나열했으나 이번 글은 테스트 시간 단축에 초점을 두어 작성할 것이다. 이 글에서 진행하는 테스트 최적화 방법은 필자의 뇌피셜에 기반한것이다. 하지만 그러면 어떠한가. 결과만 유의미하다면…

테스트 최적화

테스트 중에서 가장 시간이 많이 걸리는 테스트를 고르자면 당연히 인수테스트다. 그리고 그 다음은 통합 테스트다. 사실 Mock을 사용하는 슬라이스 테스트나 유닛 테스트는 최적화 할만한 요소가 그리 없다고 생각된다. 따라서 최적화 기법 또한 인수테스트와 통합테스트를 기준으로 작성하도록 하겠다.

[테스트 최적화 실험에 사용한 코드]

https://github.com/bperhaps/TestOptimizationStudyTest

모든 방법에 대한 테스트는 위 레포지토리에서 진행하였으며 1편에서는 간략한 방법론에 대하여 논하고 2편에서는 자세한 설정법을 알아보도록 한다.

  1. DirtiesContext 제거

현재 깃들다의 인수테스트는 DirtiesContext를 기본적으로 사용하고 있다. 심지어는 몇몇 통합테스트에서도 DirtiesContext를 사용한다. 그렇다. 사실 이게 제일 큰 문제다. DirtiesContext는 매 테스트케이스 마다 스프링 컨텍스트 전체를 내렸다가 다시 올린다.이는 결고 가벼운작업이 아니며 어쩌면 스프링 전체 프로세스중에서 가장 무거운 작업일 수도 있다. 따라서 DirtiesContext만 제거하더라도 테스트 속도는 비약적으로 증가할 것이다.

아마 DirtiesContext를 사용한 가장 큰 이유는, 데이터베이스의 초기화 때문일 것이다. 보통 DirtiesContext를 사용하는 이유는 빈의 상태를 변경시키는 테스트를 진행했을 때 이를 초기화 하기 위함이다. 하지만 깃들다의 경우 모든 빈들을 불변객체로 유지하기 때문에 빈의 상태를 변경시킬 일은 없다.

따라서 DirtiesContext를 제거하여 테스트 케이스들이 스프링 컨텍스트를 공유하여 사용할 수 있도록 하겠다.

위에 서술한 테스트 레포지토리를 통해 간략한 테스트를 진행해 보았다.

image image

DirtiesContext만 제거했을 뿐인데 의미있는 시간 시간 단축이 이루어졌다.

컨텍스트를 새로 로드하는 오버헤드는 상당히 크며 적절한 이유 없이 DirtiesContext를 사용하는 행위는 스프링 테스트의 Context Cache 이점을 전혀 이용하지 못한다. 따라서 DirtiesContext는 꼭 필요할때가 아니면 사용을 지양하는것이 좋다.

  1. 테스트 환경을 통일시킨다.

스프링 테스트가 제공하는 Application Context Cache기능을 제대로 활용하기 위해서는 테스트의 Configuration이 다르면 스프링은 새로운 Context를 올린다는 사실을 알아야한다. 또한 이를 통해 테스트의 속도를 더욱 끌어올릴 수 있다.

  1. 조회관련 테스트는, 미리 정의된 데이터를 통해 테스트한다.

필자가 생각하기에 또 시간적 비용이 많이 드는 부분을 고르자면, 테스트 함에 있어서 조회 테스트를 진행할 때 조회에 필요한 데이터를 set하는 부분이다. 조회 테스트를 진행하는 테스트 코드또한 만만치 않게 많기 때문에 이 데이터들을 데이터베이스에 올려놓고 조회 테스트를 시행하면 테스트를 더 빠르게 실행시킬 수 있을것이라고 생각한다.

즉, query를 위한 db와 update, create, delete를 위한 db를 분리한 뒤 테스트를 진행하겠다는 의미이다. 이것이 가능한 이유는 조회는 안전한 메소드(GET)을 사용함에 기반한다.

위 방법론을 간략하게 도식화 하면 아래와 같다.

image

매 테스트마다 조회를 위한 데이터를 추가했던 기존의 조회테스트와는 다르게, 조회 테스트에 필요한 Fixture를 초시화시에 미리 셋팅해두고 조회 관련 테스트를 진행한다. CUD 테스트는 기존과 동일하게 진행한다.

먼저 이를 적용하기 전의 테스트 시간은 위와 같이 10초가량이 소요된다.

image

적용 후

DB를 두개로 분리하여 테스트한 결과이다. 시간 거의 40%나 절감되었으며 조회에서는 역시 엄청난 성능 향상을 보였다. 물론 극단적인 케이스를 보였기때문에 이런 엄청난 성능향상폭을 보인걸수도 있다. 하지만 실제 프로덕션에 존재하는 수많은 read관련 테스트의 개수를 생각한다면, 적용시 테스트 시간을 극적으로 줄일 수 있을 것이라고 생각한다.

이번 글에서는 테스트 최적화의 방법론과 테스트 결과에 대하여 간단하게 논해 보았다. 다음글에서는 실제 프로젝트에 방법론을 적용하고, 그 결과를 살펴본다. 또한 각 방법의 자세한 방법에 대하여 논한다.