컨테이너 시작·준비 대기 — Wait 전략과 Startup 전략
컨테이너 시작·준비 대기 — Wait 전략과 Startup 전략
컨테이너를 띄운다고 해서 바로 그 안의 서비스가 사용 가능한 건 아니에요. DB는 로그에 시작 메시지를 남겨야 하고, 웹 서버는 포트가 응답해야 하죠. Testcontainers는 컨테이너가 '테스트에 유용한 상태'가 될 때까지 기다리는 Wait 전략과, 컨테이너가 원하는 실행 상태에 도달했는지 확인하는 Startup 전략을 제공해요. 이 페이지에서는 이 두 전략을 하나씩 살펴볼게요.
본문
Wait 전략 vs Startup 전략
Wait 전략: 컨테이너가 테스트에 유용한 상태에 이르렀는가. 대체로 '네트워크로 이 컨테이너와 대화할 수 있는가'로 근사되지만, 변형과 미묘한 차이가 꽤 많아요.
Startup 전략: 컨테이너가 원하는 실행 상태에 도달했는가. 거의 항상 '컨테이너가 실행 중일 때까지 기다리는 것'을 의미해요 — 컨테이너 안의 데몬 프로세스가 목표인 경우가 그렇죠. 때로는 컨테이너가 실행 상태에 도달한 뒤 종료되기를 기다려야 할 수도 있어요 — 이것이 'one shot startup' 전략이며, 데몬이 아니라 컨테이너 안에서 일회성 명령을 실행해야 하는 경우에만 써요.
Wait 전략
보통 Testcontainers는 컨테이너의 첫 번째 매핑된 네트워크 포트가 리슨을 시작할 때까지 최대 60초를 기다려요.
이 간단한 측정은 컨테이너가 사용할 준비가 되었는지에 대한 기본적인 확인을 제공해요.
public GenericContainer nginx = new GenericContainer(DockerImageName.parse("nginx:1.27.0-alpine3.19-slim")) //
.withExposedPorts(80);
기본 60초 타임아웃이 부족하다면 withStartupTimeout() 메서드로 바꿀 수 있어요.
TCP 포트가 리슨하는 것만으로는 준비 여부를 판단하기 부족하다면, 아래에 보이는 것처럼 waitingFor() 메서드에 다른 WaitStrategy 구현을 넘길 수 있어요.
HTTP Wait 전략 예시
HTTP(S) 엔드포인트가 특정 상태 코드를 반환할 때까지 기다리도록 선택할 수 있어요.
200 OK 기다리기
public GenericContainer nginxWithHttpWait = new GenericContainer(
DockerImageName.parse("nginx:1.27.0-alpine3.19-slim")
)
.withExposedPorts(80)
.waitingFor(Wait.forHttp("/"));
HTTP wait 전략의 변형도 지원돼요. 예를 들어 볼게요.
여러 가능한 상태 코드 기다리기
Wait.forHttp("/")
.forStatusCode(200)
.forStatusCode(301);
프레디킷을 만족하는 상태 코드 기다리기
Wait.forHttp("/all")
.forStatusCodeMatching(it -> it >= 200 && it < 300 || it == 401);
TLS 사용하기
Wait.forHttp("/all")
.usingTls();
Healthcheck Wait 전략 예시
사용하는 이미지가 Docker의 Healthcheck 기능을 지원한다면, 컨테이너의 healthy 상태를 wait 조건으로 바로 활용할 수 있어요.
Wait.forHealthcheck();
로그 출력 Wait 전략
어떤 상황에서는 컨테이너의 로그 출력이 준비 여부를 판단하는 간단한 방법이 돼요. 예를 들어 컨테이너 로그에서 Ready 메시지를 기다리려면 다음과 같이 해요.
public GenericContainer containerWithLogWait = new GenericContainer(DockerImageName.parse("redis:6-alpine"))
.withExposedPorts(6379)
.waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1));
다른 Wait 전략
추가 옵션은 Wait 편의 클래스나 WaitStrategy의 다양한 하위 클래스를 확인하세요.
이 옵션 중 어떤 것도 요구사항을 충족하지 못한다면, waitUntilReady()에 적절한 wait 메커니즘을 가진 AbstractWaitStrategy의 하위 클래스를 직접 만들 수 있어요. GenericContainer.waitingFor() 메서드는 유효한 WaitStrategy라면 무엇이든 받아요.
Startup check 전략
보통 Testcontainers는 컨테이너가 실행 상태에 도달했고 종료되지 않았는지 확인해요. 그렇게 하기 위해 컨테이너에 대해 inspect를 실행하고 state 파라미터를 추출해요.
모든 로직은 StartupCheckStrategy 하위 클래스에 구현되어 있어요.
Running startup 전략 예시
기본으로 사용되는 전략이에요. Testcontainers는 컨테이너가 실행 중인지만 확인해요.
IsRunningStartupCheckStrategy 클래스에 구현되어 있어요.
One shot startup 전략 예시
이 전략은 짧게만 실행되고 스스로 종료되는 컨테이너를 위한 것이에요. 그래서 성공은 컨테이너가 exit code 0으로 중지되었을 때라고 간주해요.
public GenericContainer<?> bboxWithOneShot = new GenericContainer<>(DockerImageName.parse("busybox:1.31.1"))
.withCommand(String.format("echo %s", HELLO_TESTCONTAINERS))
.withStartupCheckStrategy(
new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(3))
);
무기한 one shot startup 전략 예시
타임아웃을 부과하지 않는 one shot 전략의 변형이에요. 오래 실행되는 작업이 컨테이너 시작의 일부를 이루는 상황을 위한 것이에요.
컨테이너가 성공 또는 실패 exit code와 함께 스스로 중지될 것이라고 가정해야 해요.
public GenericContainer<?> bboxWithIndefiniteOneShot = new GenericContainer<>(
DockerImageName.parse("busybox:1.31.1")
)
.withCommand("sh", "-c", String.format("sleep 5 && echo \"%s\"", HELLO_TESTCONTAINERS))
.withStartupCheckStrategy(
new IndefiniteWaitOneShotStartupCheckStrategy()
);
최소 실행 시간 startup 전략 예시
컨테이너가 실행 중이고 정의된 최소 기간만큼 계속 실행되어 왔는지 확인해요.
public GenericContainer<?> bboxWithMinimumDuration = new GenericContainer<>(
DockerImageName.parse("busybox:1.31.1")
)
.withCommand("sh", "-c", String.format("sleep 5 && echo \"%s\"", HELLO_TESTCONTAINERS))
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(1))
);
다른 startup 전략
이 옵션 중 어떤 것도 요구사항을 충족하지 못한다면, waitUntilStartupSuccessful()에 적절한 startup check 메커니즘을 가진 StartupCheckStrategy의 하위 클래스를 직접 만들 수 있어요. 혹은 그대로 두고 상태를 주기적으로 확인하려면 checkStartupState(DockerClient dockerClient, String containerId)만 구현해도 돼요.
다른 컨테이너에 의존하기
때로는 컨테이너가 자기 자신을 시작하기 전에 다른 컨테이너가 준비되기를 기다려야 해요. 예를 들어 애플리케이션 컨테이너가 링크할 수 있으려면 데이터베이스가 먼저 시작되어야 하는 경우가 있죠. dependsOn 메서드로 한 컨테이너가 다른 컨테이너에 의존한다고 알려줄 수 있어요.
public GenericContainer<?> redis = new GenericContainer<>("redis:6-alpine").withExposedPorts(6379);
@Rule
public GenericContainer<?> nginx = new GenericContainer<>("nginx:1.27.0-alpine3.19-slim")
.dependsOn(redis)
.withExposedPorts(80);
더 알아보기
- 컨테이너 기반 테스트 시작하기 — GenericContainer로 컨테이너 만들기
- 네트워킹과 컨테이너 통신 — 포트 노출과 호스트 통신
- 컨테이너 로그 읽기