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

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

2021, Oct 07    

안녕하세요 깃들다의 손너잘 입니다.

이 글을 시작하기 전에 저희 프로젝트의 도메인에 대한 설명이 필요할 것 같은데요… 그냥 인스타그램이라고 생각하시면 편합니다! 그러면 글 시작하겠습니다!

드디어 제가 적용한 마지막 테스트 최적화 방법에 대한 글을 작성하게 되었네요..!

마지막으로 제가 적용시킨 방법은 인수테스트의 조회용 테스트와 그 외 테스트를 분리하여 진행하는것입니다. 이게 무슨소리인가..? 하지요..? 일단, 이에 대한 자세한 설명을 하기 전에 인수테스트의 어느 부분이 문제였는지를 설명해 보겠습니다.

전체 테스트 시간을 봤을 때 가장 오랜 시간을 차지하는 테스트는 인수테스트 입니다. 실제로 HTTP 요청을 전송하여 테스트를 진행하기 때문에 그만큼의 딜레이가 발생하기 때문입니다. 따라서 인수테스트의 속도를 최대한 빠르게 만들면, 전체 테스트 시간도 비약적으로 줄어들것이라는 생각을 하게 되었습니다.

테스트 최적화와 관련된 이런저런 글도 읽고 유툽도 봤는데, 대부분 WebMvcTest로 대체하여 사용하거나 Mock을 사용하는 방식이 가장 제너럴 한 것 같았습니다. 하지만 저는 인수테스트에 RestAssured를 사용하는 팀의 컨벤션을 변경하고 싶지 않았습니다. 즉 RestAssured를 그대로 사용하면서 테스트시간을 최대한 줄여보고 싶었습니다.

그래서 전체 인수테스트를 분석해본 결과, 테스트의 시간을 크게 높이는 원인이 테스트 FIxture를 통해 환경을 구성하는 부분이라는 것을 알았습니다.

테스트를 진행하기 위해서는 먼저 Fixture를 정의하고 이를 이용해 테스트 환경을 만듦니다. 인수테스트에서는 실제로 HTTP 메서드를 통해 POST 요청을 보내는 방식으로 각 테스트 환경을 만들어내죠. 따라서 각 테스트마다 테스트 환경을 구성하기 위한 HTTP을 보내게 됩니다. 만약 테스트 환경이 복잡하고 그 환경을 이용하는 테스트가 많이 존재한다면, 각 테스트 마다 환경을 재구성하는데 많은 시간이 소요됩니다.

제가 테스트 시간을 줄일 수 있다고 생각한 부분이 바로 이부분 입니다. 스프링 테스트가 Application Context를 재활용하는것 처럼 테스트 환경도 재활용 할 수 있다면 어떨까요?

테스트 환경의 재사용

테스트 환경을 재사용 하기 위해서는 몇가지 제약사항이 필요합니다. 바로 상태가 변경되면 안된다는점이었습니다. 만일 특정 테스트로 인해서 테스트 환경의 상태가 변경된다면 다른 테스트에 영향이 일어나고 결국 테스트는 독립적이어야 한다 라는 원칙에 영향을 끼치게 됩니다.

하지만 한가지, 환경에 영향을 끼치지 않는 테스트가 있습니다. 바로 조회 관련 테스트 입니다.

조회 테스트는 HTTP GET 메소드를 사용합니다. 그리고 GET은 서버의 데이터에 아무런 영향을 끼치지 않는 안전한 메소드 임이 자명한 사실이죠.

그리고, 인수테스트에는 조회와 관련된 테스트가 상당히 존재한다는 사실도 알았습니다. 그래서 아래 그림과 같은 테스트환경을 생각해봤습니다.

image

READ DB는 읽기만 가능한 DB이고, CUD(Create, Update, Delete) DB는 상태를 변경할 수 있는 DB입니다.

기본적으로 테스트를 진행하는데 보통 인메모리DB인 h2를 많이 사용합니다. 그리고 매 테스트마다 db에 데이터를 넣고, 테스트를 진행하고, 데이터를 지우는 방식으로 테스트를 진행합니다.

하지만 테스트를 위한 전체 데이터가 이미 저장된, 상태가 변하지 않는 DB가 있다면 어떨까요? 조회와 관련된 테스트는 해당 DB를 통해서 테스트를 진행할 수 있을겁니다. 위 그림의 READ DB가 이를 나타냅니다. 그러면 조회 관련 테스트에 한해서는 테스트 환경을 매번 만들 필요가 없을 것 입니다.

그래서 위와 같은 환경을 생각하며 테스트 코드를 리팩토링 하기 시작했습니다.

테스트 코드 분리

제가 이 작업을 진행하면서 가장 먼저 진행한 작업은 조회 관련 테스트를 추출하는 것 이었습니다.

image

위와같이 조회와 관련된 테스트를 모두 추출하였고 하나의 패키지로 묶어주었습니다. 이 작업을 진행한 이유는 다음과 같습니다.

  • 각 조회 테스트에서 필요한 Fixture와 테스트 환경 파악
  • 2개의 DB 환경을 구성하기 전에 실제로 위 가설이 실제로 효과가 있는지 확인해 보기 위함

위와 같은 이유로 조회 관련 테스트를 분리하고, DB의 초기화 로직을 주석처리 시켜주었습니다. 이를 통해 각 테스트의 DB상태가 유지될 수 있도록 했습니다(전 단계에서 테스트의 Configuration을 통일시켜놨기 때문에 동일한 Application Context에서 테스트가 진행되기 때문에 가능합니다.)

그리고 리팩토링은 위 패키지를 대상으로만 진행하였고 테스트의 통과 여부와 성능 측정도 위 패키지를 대상으로만 진행했습니다(어쩌피 이 최적화의 목적은 조회 관련 Acceptance Test테스트에 존재하니까요!).

지켜야 할 것

이 방법을 적용하기전에 스스로 제약사항을 몇가지 걸었습니다. 그것은, 인수테스트의 성질을 유지하자라는 것 입니다.

  • 인수테스트는 시나리오 테스트 입니다.
    • 인수테스트는 실제 사용자의 시나리오를 테스트합니다. 따라서 DB의 상태를 DML을 통해 직접 구성하지 않습니다. 만일 시나리오에 로그인을 해야한다는 조건이 있다면, 첫 요청에 한에서는 실제로 HTTP 요청을 전송하여 Fixture를 셋팅합니다.
    • 만일 테스트 A에서 HTTP 요청으로 사용자 로그인을 진행하여 Access Token을 받아왔고, 테스트 B에서 로그인요청이 시나리오에 포함되어 있어 그 토큰을 에서 사용한다면, 테스트 B에 대한 시나리오를 제대로 수행했다고 말할 수 있을까요? 저는 그렇다고 결론을 내렸습니다. 이미 저장된 token을 사용했고 HTTP 요청을 날리지 않으니 아니라고 말할수도 있지만, 테스트 B에서 사용한 token은 동일한 서버로 부터 HTTP 요청으로 발급받은 token이기 때문에 이는 로그인이라는 시나리오를 휼륭이 수행한 상태라고 생각할 수 있습니다.
  • 인수테스트는 문서의 목적이 있습니다.
    • 인수테스트는 테스트를 읽을 수 있어야 한다고 생각합니다. 따라서 테스트 A에서 로그인을 수행하고 토큰을 이미 가지고 있다 하더라도, 테스트 B에서 로그인 시나리오를 진행할 때 로그인을 한다는 시나리오를 코드로서 표현할 수 있어야 한다고 생각합니다.

앞으로 진행되는 리팩토링은 위의 제약사항을 지키면서 작성하였습니다.

로그인 로직 재사용

가장 먼저 로그인과 관련된 리팩토링을 진행했습니다. 로그인 또한 API 호출이 필요하므로 이미 로그인 된 유저를 재사용 가능하게하면 각 테스트에서 로그인 관련 HTTP 호출을 제거하고 테스트 속도를 올릴 수 있을것이라 생각했습니다.

image

먼저 TUser라는 테스트용 유저 데이터를 만들었습니다. 유저는 깃들다 백엔드 팀원 5명의 이름으로 하였고 token를 필드로 가지고 있을 수 있도록 했습니다. 그리고 로그인요청 시 token이 없다면 새로운 HTTP 로그인 요청을 날리고, token이 있다면 token을 재사용합니다.

image

코드에서는 위와 같이 표현됩니다.

테스트 코드에 존재하는 모든 로그인 로직을 위 로직으로 변경한뒤 테스트를 다시 실행시켜봤습니다.

image

image

대략 14~15초가 소요되던 테스트가 대략 12~13초로 줄어들었습니다. 반복되는 로그인 부분을 제거함으로서 약 2초정도의 테스트 시간 단축이 가능해졌습니다.

User의 Follow, Following 관계 구성

다음으로 진행한 부분은, 유저간의 Follow, Following 관계를 만드는 것 입니다. Follow, Following 관련 테스트를 하기 위해서 매 테스트 마다 유저를 새로 만들고, Follow, Following 관계를 새로 구성하는 코드가 테스트에 중복되어 있었습니다.

이 부분을 제거하기 위해서는 모든 테스트에서 공통으로 사용하는 Follow, Following 관계도가 필요했고 이는 아래와 같습니다.

image

총 5명의 유저와 위와 같은 팔로잉 관계를 맺도록 했습니다.

image

팔로우 관계는 코드로 위와같이 표현되며, 이 역시 이미 팔로우, 팔로워 관계면 실제 요청은 날리지 않는 식으로 만들었습니다.

image

이 작업을 완료한 뒤 테스트를 돌려보니 9~10초 사이의 시간이 소요되는것을 확인했습니다. 팔로우, 팔로잉 관계를 만들어 주는 코드가 제거됨에 따라 테스트 속도가 아주 만족스럽게 증가하였습니다!

  1. POST, LIKE, COMMENT

마지막으로 POST, LIKE, COMMENT를 정의해 보겠습니다. POST, LIKE, COMMENT또한 각 도메인의 검색, 조회등 조회에 대한 테스트를 할 때 마다 매번 Fixture를 생성하고 DB에 저장하는 요청을 보내는 요청이 중복됩니다.

3가지 도메인에 대한 작업을 동시에 진행한 이유는 특정 도메인에 대한 테스트가 아닌, 전체 인수테스트 시간을 측정하고싶어서입니다. LIKE와 COMMENT의 경우, POST와 모두 연관되어 있습니다. 따라서 만일 POST만 공통 요청 제거 작업을 진행하면 제거작업이 진행되지 않은 LIKE, COMMENT관련 테스트에 영향이 가면서 관련 테스트가 실패하게 됩니다. 따라서 POST와 함께 LIEK, COMMENT에 대한 정의를 한번에 모두 진행했습니다.

image

정의된 환경는 위와 같습니다. POST에 대해 LIKE와 COMMENT가 어떻게 달려있는지, 누가 POST를 생성했는지를 나타내고 있습니다.

image

위 그림을 정의하기 위한 TPost라는 Post Fixture를 정의하고 각 Post의 상태를 저장할 수 있도록 했습니다. 위에서 작성한 TUser처럼, 최초 Post 생성, 좋아요 표시, 댓글 달기 요청시에는 실제 HTTP 요청을 통해 데이터를 셋팅하고 DB의 상태가 어떻게 되어있는지는 객체에 표시하게 하였습니다. 그리고 동일한 요청이 들어올 경우 객체의 상태를 확인하여 이미 셋팅되어 있다면 HTTP요청을 하지 않도록 구성했습니다.

image

코드는 이런식으로 정의됩니다. 모든 환경는 위의 환경 그림에 의거하여 정의됩니다. DB의 상태가 항상 그림의 상태와 동일해야함을 약속했기 때문이죠.

image

엄청난 시간단축이 발생했습니다. 3초 후반에서 4초 초반대를 지속적으로 기록합니다. 처음 14~15초였던 테스트 시간에 비하면 정말 비약적인 발전을 이뤄냈습니다!.

이번 글에서는 인수테스트의 조회용 테스트와 그외 테스트를 분리하면 테스트의 시간이 줄어든다는것을 확인해 봤습니다. 마지막 다음 글에서는 실제로 2개의 DB환경을 구성하고 조회 테스트와 그 외의 테스트마다 서로 다른 DB를 사용하도록 하는 방법에 대하여 작성해 보겠습니다.

긴 글 읽어주셔서 감사합니다.