기존 프로젝트에서 테스트 수행 시간을 높여보자!

📌 시작하기 앞서

토스에서 'SLASH 21'이라는 세미나를 듣게 되었다. 이응준님의 '테스트 커버리지 100%'라는 주제를 듣고, 정리해봤었다. 90% 이상을 유지해본 경험은 있다. 100%는 정말 힘들 수 있겠다는 생각이 들었다. 세미나를 통해 가장 공감했던 부분은 '테스트 수행 시간'이다. 지난 프로젝트가 떠오르면서 나 또한 테스트 수행 시간으로 인해 생산성이 떨어진 경험이 있다. 세미나를 통해 듣고, 정리하고 지나가는 것이 아니라, 직접 적용해봐야 내것으로 만들 수 있겠다는 생각이 들었다. 그래서 이번 글은 지난 프로젝트를 이용하여 테스트 수행 시간을 높이는 과정에 대해 글로 정리해보려고 한다. 시간이 얼마나 걸릴지는 모르겠지만, 삽질을 통해 내것으로 만들어보자!

📌 세미나에서 얻은 힌트를 적용해보자!

적용할 것들

스프링 애플리케이션 컨텍스트 로딩하는 데 걸리는 시간을 줄이자.
인텔리제이에 내장 async-profiler를 이용해서 테스트 코드에 대한 성능 프로파일링을 해보자.
Junit 테스트 설정을 클래스 혹은 함수 단위로 병렬로 설정해서 비교해보자.
노트북을 바꿨더니 2.5배가 빨라졌다..? 역시 돈인가..? 돈 많이 벌면 적용해보자!

알아보고 적용할 수 있을지 판단해야 할 것들

Handlebars 컴파일 → 캐시 적용
MockK → 필수적이지 않으면 모두 제거
Byte Buddy 초기화 → 테스트에서 사용 중단

이미 적용해본 것

Jacoco를 이용하여 커버리지가 목표된 수치를 충족시키지 못하면, 빌드가 되지 않게 하라.
프로젝트 초기부터 높은 상태의 커버리지를 유지하자. → 90%는 유지하고자 했기 때문에 패스!

📌 어떤 목표를 잡을까?

테스트 커버리지 100% 달성하기
테스트 수행 시간 N% 향상시키기

📌 현재 나의 상황은 어떤가?

개인 프로젝트(교내 챗봇 서비스)

라인 커버리지를 90% 유지하는 것이 목표였기에 100%까지 올리는 데 큰 어려움은 없을 것 같다. 빠진 테스트를 찾아보고, 수행 시간을 최대한 낮춰보려고 한다.
클래스 커버리지 94%, 메서드 커버리지 89%, 라인 커버리지 90%
총 88개의 테스트를 수행하는 데 걸리는 시간 7.753s

📌 테스트 수행 속도 끌어올리기

📌 'async-profiler'를 이용하여 테스트 코드 성능 프로파일링해보기

'async-profiler'가 뭘까?

'async-profiler'는 인텔리제이에 내장되어 있는 프로파일러이다. 애플리케이션이 실행되는 방식과 메모리, CPU 리소스가 할당되는 방식을 알 수 있다. 이를 통해 성능 문제 및 병목 현상을 찾고 해결하는 데 도움된다.
macOS 환경에서는 설정 필요 없이, 즉시 작동할 수 있다. 그러나 리눅스 환경에서는 커널 옵션을 조정해야 하므로, 리눅스 환경이라면 아래 링크를 참고하자.

Agent options 설정

IntelliJ IDEA는 CPU 프로파일러와 메모리 할당 프로파일러를 제공한다. 아래 그림을 통해 들어가게 되면 옵션을 설정할 수 있다. 그러나 나와 같은 초심자라면 일단은 세팅된 값으로 시작해보자.
IntelliJ IDEA → Preferences
Build, Execution, Deployment → Java Profiler
3번에서 이미 설정되어 있지만, 다른 값으로 설정하려면 아래 링크를 참고하자.

실행하기

TODO

📌 Junit 테스트 설정을 클래스/함수 단위로 병렬로 수행하도록 설정

기본적으로 JUnit Jupiter 테스트는 단일 스레드로 순차적으로 실행된다. 실행 속도를 높이기 위해 버전 5.3부터 병렬로 테스할 수 있는 기능이 추가됐다. 그러나 아직 시험중인 단계라고 하니 유의하며 사용하자.

병렬로 설정하기

먼저 junit.jupiter.execution.parallel.enabledtrue로 설정해야 한다. 그러나 이렇게 설정했다고 해서 병렬로 실행되지는 않는다. 병렬로 실행하기 위해서는 2가지 실행 모드 중에 하나를 택해야 한다.

실행 모드 선택하기

실행 모드는 SAME_THREADCONCURRENT가 존재한다. 디폴트는 SAME_THREAD이다.
SAME_THREAD
부모가 사용하는 동일한 스레드에서 강제 실행한다.
@BeforeAll 또는 @AfterAll을 포함하는 테스트 클래스 방법
CONCURRENT
리소스 락이 동일한 스레드에서 강제 실행하지 않는 한 동시에 실행한다.

특정 클래스만 병렬로 실행

@Execution 어노테이션을 이용하면 된다.

예시) 모든 테스트 병렬로 실행시키기

junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent
Plain Text

동기화에 신경쓰려면..?

동기화에 신경써야 하는 경우, 클래스나 메서드에 @Execution(CONCURRENT)을 명시해줘야 한다.

최상위 클래스에 기본 설정하기

아래와 같이 설정하면 최상위 클래스에 대한 기본 설정을 할 수 있다.
junit.jupiter.execution.parallel.mode.classes.default
Plain Text

설정: 최상위 클래스는 병렬로 실행, 동일한 스레드의 메서드 실행

junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = concurrent
Plain Text

설정: 최상위 클래스를 순차적으로 실행, 해당 메서드는 병렬로 실행

junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent junit.jupiter.execution.parallel.mode.classes.default = same_thread
Plain Text

적용 후, 분석 결과

클래스 단위로 병렬로 실행했을 때, 가장 빨랐어야 했다. 그러나 결과는 예상과 완전히 벗어났다. 클래스 단위로 실행했더니 시간은 2배 이상걸렸고, 함수 단위로 실행했더니 클래스 단위보다 작지만 기본 세팅 값보다 시간이 오래 소요되는 것은 여전했다.
로그를 확인해봤다. 모든 스레드가 각각 컨텍스트를 띄우고 있었다. 그래서 하나의 컨텍스트를 띄우고, 스레드가 공유하는 방법을 찾아봤다.
TODO

스프링 애플리케이션 컨텍스트 로딩하는 시간 줄이기

TODO

📌 테스트 커버리지 100%까지 올리기

📌 참고 자료

TOP