안녕하세요, Junit-team/junit5, spring/spring-boot, apache/seata, naver/fixture-monkey 등 여러 오픈소스 프로젝트에 기여한 YongGoose입니다.
찾아보니 JUnit5에는 전 세계에서 35번째로 많이 기여를 했더라고요.
Commit 순이 아닌, Additions 순으로 하면 17번째입니다.🙂

이번 글에서는 제가 JUnit5 Vintage엔진에 병렬화를 도입한 이야기를 해보려고 합니다.
잠깐만 JUnit Vintage Engine이 뭐야..?
우선, 간단히 JUnit5의 구조 및 Vintage Engine에 대해 설명을 하겠습니다.

JUnit5은 테스트 프레임워크를 JVM에서 실행하기 위해 JUnit Platform을 제공합니다.
Platform은 주로 다음의 기능을 수행합니다.
- TestEngine API 정의 : 플랫폼에서 실행될 테스트 프레임워크를 개발할 수 있는 API를 제공합니다.
- Console Launcher 제공 : 명령줄에서 플랫폼을 실행할 수 있게 해줍니다.
- IDE 및 빌드 도구 지원 : Intellij, Eclipse, VSCode 등에서 테스트를 실행할 수 있게 지원합니다.
TestEngine은 테스트를 발견하고 실행하는 핵심 인터페이스입니다.
- 테스트 발견(Discovery) : Launcher, IDE가 테스트 대상을 인식할 수 있게 도와줍니다.
- 테스트 실행(Execution) : 발견된 테스트들을 실행할 수 있게 도와줍니다. (시작, 완료, 성공, 실패등의 이벤트를 발생시킵니다)
JUnit Jupiter는 JUnit5의 새로운 프로그래밍 및 확장 모델을 제공합니다.
- Jupiter TestEngine을 통해 최신 방식으로 테스트를 작성하고 확장할 수 있게 도와줍니다.
JUnit Vintage는 기존의 JUnit 3, JUnit 4 기반 테스트를 JUnit 5 플랫폼에서 실행할 수 있게 해줍니다.
JUnit user-guide에서 더욱 자세히 볼 수 있습니다.
https://junit.org/junit5/docs/current/user-guide/
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo
junit.org
그래서 어떤 기여를 했는데?
배경에 대해 자세히 설명하자면, JUnit Jupiter-Engine에는 테스트를 병렬로 실행시킬 수 있는 기능이 있습니다.
https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo
junit.org
하지만 Vintage-Engine에는 테스트를 병렬로 실행시킬 수 있는 기능이 없습니다.
그래서 모든 테스트는 순차적으로 실행이 됩니다.
아래의 이슈에서 자세히 볼 수 있습니다.
Issue: https://github.com/junit-team/junit5/issues/2229
Support some level of parallelization in junit-vintage-engine · Issue #2229 · junit-team/junit5
Goal Add support for some level of parallelization inside the Junit Vintage engine. Ideally the level of configuration should be similar to maven surefire parallel options. Why When including junit...
github.com
아래의 시퀀스 다이어그램처럼, for 반복문을 사용하여 테스트 요소를 순차적으로 실행하고, 처리한 요소를 제거하는 방식으로 진행됩니다.

아래의 사진과 같이 JUnit Vintage 엔진에서도 병렬화를 지원해 주라는 요구가 많아 제가 작업을 하게 되었습니다.

우선 병렬화를 도입하기 위해 어디에 도입을 하면 좋을지에 대해 생각을 해보았습니다.

이때, 이슈에서 JUnit5의 메인테이너인 Marc가 RunnerExecutor를 병렬로 호출하라는 힌트를 주는 메시지를 확인했습니다.
정확히는 아래의 로직에 병렬화를 적용하는 것입니다.

병렬화를 위해 스레드 풀을 생성하여 여러 작업을 동시에 실행하는 로직을 구현하기로 결정했습니다.
초기에는 테스트라는 큰 단위를 여러 개의 작은 작업으로 분할하여 실행함으로써, ForkJoinPool이 제공하는 work-stealing 기능을 활용해 성능 향상을 기대하며 이를 적용했습니다.
그러나 메인테이너님께서는 ForkJoinPool 사용이 현재 구조에서는 별다른 이점을 제공하지 않는다는 의견을 주셨습니다.

그래서 왜 ForkJoinPool을 사용하는 것이 이점이 없을까..?라는 고민을 해보았습니다.
우선, ForkJoinPool은 하나의 Task가 여러 개의 SubTask로 나뉘는 작업일 때 효율적입니다.
여러 개의 SubTask로 나눠야 다른 스레드가 더 이상 처리할 작업이 없을 때 작업을 가져올 수 있기 때문입니다.

처음에는 테스트(클래스)가 여러 작업(메서드)으로 나누어져 병렬 실행될 것으로 기대하며 작업을 진행했습니다. 하지만 자세히 살펴보니, 실제로는 각 테스트(클래스)가 하나의 독립된 작업으로 구성되어 있다는 것을 알게 되었습니다. 이런 이유로 메인테이너님께서는 ForkJoinPool을 사용하는 것에는 별다른 이점이 없다고 말씀하셨던 것입니다.
결과적으로 고정된 크기를 가지는 newFixedThreadPool을 통해 스레드 풀을 생성했습니다.


하지만 하나의 테스트를 전체적으로 실행하는 것보다 각 메서드를 개별적으로 실행하는 것이 성능 면에서 분명히 이점이 있을 것이라고 생각했습니다. 그래서 메인테이너님께 코멘트를 통해 제안을 했습니다.

코멘트의 길이가 길어 한 페이지에 모두 담기지가 않아 링크를 별도로 남깁니다.
https://github.com/junit-team/junit5/pull/4135#issuecomment-2543164929
Support parallelization in junit-vintage-engine by YongGoose · Pull Request #4135 · junit-team/junit5
Overview Resolves #2229 This PR implements parallel execution support for the JUnit Vintage engine, addressing issue #2229. Implemented concurrent execution of test descriptors when parallel execu...
github.com
제안을 한 덕분에 저는 별도의 PR을 통해 메서드 단위 병렬화를 진행할 수 있었습니다.
다음 글에서 자세히 소개하겠습니다.
아래의 코드는 비동기를 통해 병렬화를 적용한 부분입니다.
private boolean executeInParallel(VintageEngineDescriptor engineDescriptor,
EngineExecutionListener engineExecutionListener, ExecutionRequest request) {
ExecutorService executorService = Executors.newFixedThreadPool(getThreadPoolSize(request));
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
TestDescriptor descriptor = iterator.next();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
runnerExecutor.execute((RunnerTestDescriptor) descriptor);
}, executorService);
futures.add(future);
iterator.remove();
}
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]));
boolean wasInterrupted = false;
try {
allOf.get();
}
catch (InterruptedException e) {
logger.warn(e, () -> "Interruption while waiting for parallel test execution to finish");
wasInterrupted = true;
}
catch (ExecutionException e) {
throw ExceptionUtils.throwAsUncheckedException(e.getCause());
}
finally {
shutdownExecutorService(executorService);
}
return wasInterrupted;
}
전체적인 설명은 아래의 이미지에서 볼 수 있습니다.
포트폴리오에서 발췌한 이미지입니다.

전체적인 코드 및 제가 메인테이너님과 나눈 대화를 볼 수 있는 Pull Request입니다.
Pull Request: https://github.com/junit-team/junit5/pull/4135
Support parallelization in junit-vintage-engine by YongGoose · Pull Request #4135 · junit-team/junit5
Overview Resolves #2229 This PR implements parallel execution support for the JUnit Vintage engine, addressing issue #2229. Implemented concurrent execution of test descriptors when parallel execu...
github.com
그래서 지금 사용할 수 있어?
어제, JUnit5 5.12 버전이 배포되었습니다.
5.12 버전에서 제가 생성한 기능(JUnit-vintage 클래스/메서드 단위 병렬화)을 사용할 수 있습니다.
JUnit-vintage 병렬화 기능을 사용하고 싶은데 질문이 있으신 분은 깃허브에 있는 이메일로 연락 주시면 감사하겠습니다.
또한 커피챗, 오픈소스 관련 얘기도 환영합니다. 🙂

https://junit.org/junit5/docs/5.12.0/release-notes/
JUnit 5 Release Notes
Date of Release: September 25, 2024 Scope: Bug fixes and enhancements since 5.11.0 For a complete list of all closed issues and pull requests for this release, consult the 5.11.1 milestone page in the JUnit repository on GitHub. JUnit Platform Bug Fixes Fi
junit.org
안녕하세요, Junit-team/junit5, spring/spring-boot, apache/seata, naver/fixture-monkey 등 여러 오픈소스 프로젝트에 기여한 YongGoose입니다.
찾아보니 JUnit5에는 전 세계에서 35번째로 많이 기여를 했더라고요.
Commit 순이 아닌, Additions 순으로 하면 17번째입니다.🙂

이번 글에서는 제가 JUnit5 Vintage엔진에 병렬화를 도입한 이야기를 해보려고 합니다.
잠깐만 JUnit Vintage Engine이 뭐야..?
우선, 간단히 JUnit5의 구조 및 Vintage Engine에 대해 설명을 하겠습니다.

JUnit5은 테스트 프레임워크를 JVM에서 실행하기 위해 JUnit Platform을 제공합니다.
Platform은 주로 다음의 기능을 수행합니다.
- TestEngine API 정의 : 플랫폼에서 실행될 테스트 프레임워크를 개발할 수 있는 API를 제공합니다.
- Console Launcher 제공 : 명령줄에서 플랫폼을 실행할 수 있게 해줍니다.
- IDE 및 빌드 도구 지원 : Intellij, Eclipse, VSCode 등에서 테스트를 실행할 수 있게 지원합니다.
TestEngine은 테스트를 발견하고 실행하는 핵심 인터페이스입니다.
- 테스트 발견(Discovery) : Launcher, IDE가 테스트 대상을 인식할 수 있게 도와줍니다.
- 테스트 실행(Execution) : 발견된 테스트들을 실행할 수 있게 도와줍니다. (시작, 완료, 성공, 실패등의 이벤트를 발생시킵니다)
JUnit Jupiter는 JUnit5의 새로운 프로그래밍 및 확장 모델을 제공합니다.
- Jupiter TestEngine을 통해 최신 방식으로 테스트를 작성하고 확장할 수 있게 도와줍니다.
JUnit Vintage는 기존의 JUnit 3, JUnit 4 기반 테스트를 JUnit 5 플랫폼에서 실행할 수 있게 해줍니다.
JUnit user-guide에서 더욱 자세히 볼 수 있습니다.
https://junit.org/junit5/docs/current/user-guide/
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo
junit.org
그래서 어떤 기여를 했는데?
배경에 대해 자세히 설명하자면, JUnit Jupiter-Engine에는 테스트를 병렬로 실행시킬 수 있는 기능이 있습니다.
https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo
junit.org
하지만 Vintage-Engine에는 테스트를 병렬로 실행시킬 수 있는 기능이 없습니다.
그래서 모든 테스트는 순차적으로 실행이 됩니다.
아래의 이슈에서 자세히 볼 수 있습니다.
Issue: https://github.com/junit-team/junit5/issues/2229
Support some level of parallelization in junit-vintage-engine · Issue #2229 · junit-team/junit5
Goal Add support for some level of parallelization inside the Junit Vintage engine. Ideally the level of configuration should be similar to maven surefire parallel options. Why When including junit...
github.com
아래의 시퀀스 다이어그램처럼, for 반복문을 사용하여 테스트 요소를 순차적으로 실행하고, 처리한 요소를 제거하는 방식으로 진행됩니다.

아래의 사진과 같이 JUnit Vintage 엔진에서도 병렬화를 지원해 주라는 요구가 많아 제가 작업을 하게 되었습니다.

우선 병렬화를 도입하기 위해 어디에 도입을 하면 좋을지에 대해 생각을 해보았습니다.

이때, 이슈에서 JUnit5의 메인테이너인 Marc가 RunnerExecutor를 병렬로 호출하라는 힌트를 주는 메시지를 확인했습니다.
정확히는 아래의 로직에 병렬화를 적용하는 것입니다.

병렬화를 위해 스레드 풀을 생성하여 여러 작업을 동시에 실행하는 로직을 구현하기로 결정했습니다.
초기에는 테스트라는 큰 단위를 여러 개의 작은 작업으로 분할하여 실행함으로써, ForkJoinPool이 제공하는 work-stealing 기능을 활용해 성능 향상을 기대하며 이를 적용했습니다.
그러나 메인테이너님께서는 ForkJoinPool 사용이 현재 구조에서는 별다른 이점을 제공하지 않는다는 의견을 주셨습니다.

그래서 왜 ForkJoinPool을 사용하는 것이 이점이 없을까..?라는 고민을 해보았습니다.
우선, ForkJoinPool은 하나의 Task가 여러 개의 SubTask로 나뉘는 작업일 때 효율적입니다.
여러 개의 SubTask로 나눠야 다른 스레드가 더 이상 처리할 작업이 없을 때 작업을 가져올 수 있기 때문입니다.

처음에는 테스트(클래스)가 여러 작업(메서드)으로 나누어져 병렬 실행될 것으로 기대하며 작업을 진행했습니다. 하지만 자세히 살펴보니, 실제로는 각 테스트(클래스)가 하나의 독립된 작업으로 구성되어 있다는 것을 알게 되었습니다. 이런 이유로 메인테이너님께서는 ForkJoinPool을 사용하는 것에는 별다른 이점이 없다고 말씀하셨던 것입니다.
결과적으로 고정된 크기를 가지는 newFixedThreadPool을 통해 스레드 풀을 생성했습니다.


하지만 하나의 테스트를 전체적으로 실행하는 것보다 각 메서드를 개별적으로 실행하는 것이 성능 면에서 분명히 이점이 있을 것이라고 생각했습니다. 그래서 메인테이너님께 코멘트를 통해 제안을 했습니다.

코멘트의 길이가 길어 한 페이지에 모두 담기지가 않아 링크를 별도로 남깁니다.
https://github.com/junit-team/junit5/pull/4135#issuecomment-2543164929
Support parallelization in junit-vintage-engine by YongGoose · Pull Request #4135 · junit-team/junit5
Overview Resolves #2229 This PR implements parallel execution support for the JUnit Vintage engine, addressing issue #2229. Implemented concurrent execution of test descriptors when parallel execu...
github.com
제안을 한 덕분에 저는 별도의 PR을 통해 메서드 단위 병렬화를 진행할 수 있었습니다.
다음 글에서 자세히 소개하겠습니다.
아래의 코드는 비동기를 통해 병렬화를 적용한 부분입니다.
private boolean executeInParallel(VintageEngineDescriptor engineDescriptor,
EngineExecutionListener engineExecutionListener, ExecutionRequest request) {
ExecutorService executorService = Executors.newFixedThreadPool(getThreadPoolSize(request));
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
TestDescriptor descriptor = iterator.next();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
runnerExecutor.execute((RunnerTestDescriptor) descriptor);
}, executorService);
futures.add(future);
iterator.remove();
}
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]));
boolean wasInterrupted = false;
try {
allOf.get();
}
catch (InterruptedException e) {
logger.warn(e, () -> "Interruption while waiting for parallel test execution to finish");
wasInterrupted = true;
}
catch (ExecutionException e) {
throw ExceptionUtils.throwAsUncheckedException(e.getCause());
}
finally {
shutdownExecutorService(executorService);
}
return wasInterrupted;
}
전체적인 설명은 아래의 이미지에서 볼 수 있습니다.
포트폴리오에서 발췌한 이미지입니다.

전체적인 코드 및 제가 메인테이너님과 나눈 대화를 볼 수 있는 Pull Request입니다.
Pull Request: https://github.com/junit-team/junit5/pull/4135
Support parallelization in junit-vintage-engine by YongGoose · Pull Request #4135 · junit-team/junit5
Overview Resolves #2229 This PR implements parallel execution support for the JUnit Vintage engine, addressing issue #2229. Implemented concurrent execution of test descriptors when parallel execu...
github.com
그래서 지금 사용할 수 있어?
어제, JUnit5 5.12 버전이 배포되었습니다.
5.12 버전에서 제가 생성한 기능(JUnit-vintage 클래스/메서드 단위 병렬화)을 사용할 수 있습니다.
JUnit-vintage 병렬화 기능을 사용하고 싶은데 질문이 있으신 분은 깃허브에 있는 이메일로 연락 주시면 감사하겠습니다.
또한 커피챗, 오픈소스 관련 얘기도 환영합니다. 🙂

https://junit.org/junit5/docs/5.12.0/release-notes/
JUnit 5 Release Notes
Date of Release: September 25, 2024 Scope: Bug fixes and enhancements since 5.11.0 For a complete list of all closed issues and pull requests for this release, consult the 5.11.1 milestone page in the JUnit repository on GitHub. JUnit Platform Bug Fixes Fi
junit.org