최근 약 400%가량의 테스트 성능 향상 시킨 사례로 사내 세미나를 진행하게 되었습니다
이번 포스팅에서는 Springboot 기반 WAS 에서 작성된 테스트 코드가
어떠한 과정을 거쳐 약 400%의 성능 개선을 이뤄냈는지에 대해서 다뤄볼 예정입니다
개선 수행 환경
- Springboot 2.3
- Gradle 7.1
- JUnit 4
먼저 개선을 수행한 `Workbench` 라는 프로젝트는 `No Code` 로 회사 모든 시스템을 유저가 쉽게 사용할 수 있도록 제공하는 서비스입니다
저희 부서에선 저를 포함한 2명의 개발자가 투입되어 현재 2년 정도 서비스를 고도화 및 유지보수 중이며
2년이라는 시간이 흐르는 동안 약 330개가 넘는 테스트 케이스가 작성되었습니다
서비스 개발 초기 전체 Full Test 수행에 1분 내외로 걸리던 시간은
유지보수 되지 않은 테스트 코드로 인해
Full Test 수행에 어느덧 13분에 가까운 시간이 걸리게 되었습니다
개발을 진행하면서 테스트 코드 수행을 통해 변경되는 부분과, 새로 추가되는 기능에 대한 안정감을 얻고 있던 저에겐
13분이라는 시간은 커다란 부담으로 느껴졌고, 언젠가부턴 테스트 수행을 스킵하는 일이 잦아졌습니다
이런 상황은 테스트 코드 작성의 본래 목적에 어긋난다고 판단하여, 본격적인 테스트 코드 개선 작업에 들어갔습니다
테스트 코드 개선 작업은 3가지 과정에 거쳐 진행되었습니다
- Spring의 TestContext 재활용
- Gradle parallel execution
- Gradle generate test report - disable
1. Spring TestContext 재활용
Spring의 Test Context 재활용은, 테스트 성능 개선 중 가장 많은 성능 개선을 이뤄낸 부분이었습니다
Spring에서는 Test Framework로 TestContext Framework를 사용합니다
Spring 특성상, 단순 Unit 테스트를 제외하면 Spring의 Application Context를 띄워서 테스트해야 하는 경우가 대부분이고
매 테스트마다 Application Context를 띄우는 것은 매우 비효율적이기에
TestContext Framework는 재사용이 가능한 Application Context를 Caching 하여 재사용하는 기능을 제공합니다 (Caching Key - Spring Test Referrence)
Context를 재사용하는 케이스에 대해 이해하기 쉽게 설명하자면 다음과 같습니다
Caching key 구성에 필요한 Bean을 포함한 설정 정보가 모두 같고,
이전 테스트가 Context를 더럽히지(dirty) 않는다면 해당 Context를 재사용 가능하다
@MockBean 어노테이션으로 Bean을 모킹 하는 작업은
위에서 설명한 Context를 더럽히는 즉, Bean 상태를 변경하는 사항이므로
모킹한 Bean의 조합이 다른 테스트가 있으면 해당 테스트마다 Context를 새로 띄워야 합니다
바로 이 부분이 `Workbench` 프로젝트의 테스트 성능을 저하시키는 가장 큰 원인이 되는 부분이었습니다
Spring에서 제공하는 Bean을 포함한 stereotype들에 대한 테스트가 서로 얽혀있는 만큼
해당 클래스들을 모킹 해야 하는 경우가 많았고, @MockBean으로 선언한 클래스가 테스트 이곳저곳에 존재하는 구조로 되어 있었습니다
따라서 Bean을 모킹 하는 부분을 하나의 클래스로 모아, 해당 클래스를 상속받아 테스트를 수행하는 방향으로 개선을 진행하였습니다
물론 TestContext Framework에서 Context를 Caching 하는 조건이 Bean을 모킹 하는 부분만 있는 것이 아닙니다
하지만 대부분의 테스트 환경이 얼추 비슷했고, 약간은 억지로 끼워 맞춰진 테스트도 있었지만
테스트 성능 개선이라는 큰 목적 아래 어느 정도 감당 가능한 트레이드오프의 영역으로 간주하고 진행하였습니다
위 개선 사항으로 Context를 다시 띄우는 빈도가 눈에 띄게 줄었고,
테스트 시간은
13 분 -> 3분 50초로
1차 개선에서 약 60%가 넘는 성능 개선을 이뤘습니다
2. Gradle parallel execution
두 번째 개선 항목은 Gradle에서 공식적으로 지원하는 멀티 프로세스 병렬 테스트 옵션을 사용하여 진행하였습니다
주인공은 바로 maxParallelForks 옵션입니다
해당 옵션의 사용은 Gradle에서 제공하는 Optimizing Build Times 에서 하나의 항목으로 제공하고 있으며
가용 가능한 CPU 프로세서의 절반에 해당하는 값을 추천하고 있습니다
하지만 실제 운영 환경에서 테스트 수행 중에 CPU 자원을 소모하는 작업들은 앞으로도 예측이 어렵기 때문에
maxParallelForks 값은 고정 값으로 2를 주었습니다 (두 개의 프로세서를 사용할 때, 프로세서 개수 대비 성능 개선 수치가 가장 좋았기에 채택하였습니다)
참고로, 해당 옵션 사용하고 테스트에 사용되는 리소스를 공유할 경우,
각 테스트의 수행 순서로 인한 데이터 간섭 문제가 발생할 수 있습니다.
따라서 리소스에 변경을 가하는 테스트는 반드시 독립적으로 작성하길 추천합니다
2번 개선 사항의 결과로 개선된 테스트 시간은
3분 50초 -> 3분 18초로
1차 개선으로부터 약 10% 더 성능 개선을 이뤘습니다
3. Gradle generate test report - disable
세 번째 개선 사항은 Gradle의 테스트 결과로 생성되는 테스트 결과 Report 생성 기능을 disable 하는 것입니다
해당 내용은 Gradle에서 제공하는 Optimizing Build Times 문서에서도 제안하고 있는 사항으로
Report를 확인하지 않는다면, 해당 기능을 disable 할 것을 권장하고 있습니다
`Workbench` 프로젝트에선 테스트 Report를 확인하지 않아 해당 옵션을 끄는 것으로 진행했습니다
추가로 Conditional 하게 enable 하는 기능도 제공하기에 Report가
선택적으로 필요하다면 링크 를 참조해 보세요
3번 개선 사항으로 개선된 테스트 시간은
2번 개선사항으로부터 약 2초 줄어든 3분 16초를 기록했습니다
드라마틱한 개선 사항은 아니라.. 당장은 켜놔도 괜찮지 않을까 하는 생각은 들었습니다 😅
성능 개선 결과
개선 전 | 개선 후 | |
Local Test (로컬 개발 환경) | 12분 30초 | 3분 16초 |
Jenkins Test (실제 운영 환경) | 20분 | 5분 30초 |
테스트 성능 개선 결과
실제로 수치상 약 400% 정도의 성능 개선 효과를 얻었습니다
이는 하루 한 명의 개발자가 로컬 환경 기준 10번의 테스트를 수행한다는 전제 하에
테스트 수행에 필요한 시간을 120분 -> 30분으로 약 90분의 시간을 save 할 수 있는 수치이며
6명의 팀원도 동일한 상황이라고 가정했을 경우
하루에 총 540분(9시간)의 인적 리소스를 절약할 수 있는 수치입니다
물론 테스트 수행 시간 동안 개발자가 Queue에서 대기하는 것 마냥 넋 놓고 있지는 않을 것이기에
위에 표시된 수치는 어느 정도 과장이 있을 수 있습니다.
하지만 테스트 수행 시간으로부터 받던 압박으로부터 해방된 것처럼
부수적으로 얻은 심리적 개선 효과도 무시하지 못할 것입니다
따라서 이번 테스트 성능 개선은 성공적으로 마무리되었다고 표현하고 싶습니다
긴 글 읽어주셔서 감사합니다! 😄
참고 문헌
'실무' 카테고리의 다른 글
[2022-12-27] 사내 TDD 세미나 발표 회고 (0) | 2022.12.28 |
---|---|
사용자 민감 정보 다루기 (with. TextEncryptor) (2) | 2022.05.15 |
실무 카테고리 Open (0) | 2022.05.07 |
댓글