테스트컨테이너(TestContainer)의 병렬 실행에 대해 작성한 글입니다. 퍼가실 땐 출처를 밝혀주세요. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.
1. 테스트컨테이너의 문제점
테스트컨테이너의 가장 큰 문제점은 실행 속도입니다. 아래는 약 15개 정도의 통합/문서화 테스트를 포함한 빌드한 시간인데, 1분 26초 정도가 나온 것을 볼 수 있습니다. 만약 테스트가 많아진다면 빌드 속도가 가파르게 증가합니다.
이는 내부적으로 도커 컨테이너를 띄우기 때문인데, 도커 컨테이너를 띄우고, 컨테이너가 뜰 때까지 기다리는 작업이 포함돼 있어 상당히 느립니다. 만약 테스트가 순차적으로 실행될 경우, 한 테스트가 끝날 때까지 다음 테스트는 실행되지 못하므로 전체 빌드 시간이 증가하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Data
public class GenericContainer<SELF extends GenericContainer<SELF>>
extends FailureDetectingExternalResource
implements Container<SELF>, AutoCloseable, WaitStrategyTarget, Startable {
......
protected void doStart() {
try {
......
Unreliables.retryUntilSuccess(
startupAttempts,
() -> {
......
// 컨테이너 구동
tryStart(startedAt);
return true;
}
);
} catch (Exception e) {
throw new ContainerLaunchException("Container startup failed", e);
}
}
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Data
public class GenericContainer<SELF extends GenericContainer<SELF>>
extends FailureDetectingExternalResource
implements Container<SELF>, AutoCloseable, WaitStrategyTarget, Startable {
......
private void tryStart(Instant startedAt) {
try {
// 컨테이너 생성 및 설정
CreateContainerCmd createCommand = dockerClient.createContainerCmd(dockerImageName);
......
// 컨테이너 구동
dockerClient.startContainerCmd(containerId).exec();
......
try {
// 컨테이너가 생성되지 않았다면 기다림
waitUntilContainerStarted();
} catch (Exception e) {
......
throw new ContainerLaunchException("Could not create/start container", e);
}
}
......
}
2. 해결
이 문제를 테스트 병렬 실행으로 개선할 수 있습니다.
단, 단순히 병렬로 테스트를 실행하면 안 되는데, 아무런 조건 없이 이를 실행할 때 아래와같이 무수히 많은 테스트 컨테이너가 띄워지기 때문입니다.
테스트컨테이너는 컨테이너가 띄워지기 전 내부적으로 컨테이너가 띄워져 있는지를 먼저 체크합니다. 따라서 이를 동기화해서 여러 개의 테스트 컨테이너가 띄워지지 않도록 해줘야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Data
public class GenericContainer<SELF extends GenericContainer<SELF>>
extends FailureDetectingExternalResource
implements Container<SELF>, AutoCloseable, WaitStrategyTarget, Startable {
......
private void tryStart(Instant startedAt) {
try {
String dockerImageName = getDockerImageName();
logger().debug("Starting container: {}", dockerImageName);
logger().info("Creating container for image: {}", dockerImageName);
CreateContainerCmd createCommand = dockerClient.createContainerCmd(dockerImageName);
applyConfiguration(createCommand);
createCommand.getLabels().putAll(DockerClientFactory.DEFAULT_LABELS);
boolean reused = false;
final boolean reusable;
// 변수 체크
if (shouldBeReused) {
if (!canBeReused()) {
throw new IllegalStateException("This container does not support reuse");
}
}
......
}
}
}
아래와 같이요. synchronized 키워드를 사용하면 이런 문제를 해결하고 테스트를 안전하게 병렬로 실행할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@IntegrationTest
public abstract class IntegrationTestBase {
......
@BeforeAll
static void beforeAll() {
synchronized (TestContainer.class) {
TestContainer.start();
}
}
......
}
이를 통해 성능이 1/4 단축된 것을 볼 수 있습니다. 이는 테스트가 많아질수록 가파르게 빨라집니다.
3. 정리
테스트컨테이너는 많은 장점을 가지고 있지만, 느린 구동 시간 때문에 사용하기 부담스럽다는 단점이 있습니다. 하지만 테스트를 병렬로 실행하면 이런 성능 문제를 해결할 수 있으므로 적극 활용해 볼 것을 권장해 드립니다.

