Reactor Core 테스트의 중요성
리액티브 프로그래밍 환경에서는 데이터 스트림의 어느 지점에서든 예외가 발생할 수 있습니다. 따라서 정상적인 데이터 흐름뿐만 아니라, 오류 발생 시 시스템이 어떻게 반응하고 복구하는지 검증하는 것이 애플리케이션의 안정성을 보장하는 핵심 요소입니다. reactor-test 모듈은 이러한 장애 시나리오를 시뮬레이션하고 검증할 수 있는 강력한 도구를 제공합니다.
1. StepVerifier를 활용한 오류 메시지 검증
StepVerifier는 리액티브 스트림의 종단 상태를 검증하는 기본 도구입니다. 특정 예외 메시지를 정확히 일치하는지 확인할 때 유용합니다.
Flux<Integer> numberStream = Flux.just(1, 2, 3, 4)
.concatWith(Mono.error(new IllegalStateException("잘못된 상태 발생")));
StepVerifier.create(numberStream)
.expectNext(1, 2, 3, 4)
.verifyErrorMessage("잘못된 상태 발생");
2. 특정 예외 타입 및 상세 속성 검증
단순한 메시지 비교를 넘어, 발생한 예외의 클래스 타입이나 내부 속성을 정밀하게 검증해야 할 경우가 있습니다.
Flux<String> errorStream = Flux.error(new CustomDomainException("도메인 규칙 위반", 400));
StepVerifier.create(errorStream)
.verifyErrorSatisfies(throwable -> {
assertThat(throwable).isInstanceOf(CustomDomainException.class);
assertThat(((CustomDomainException) throwable).getErrorCode()).isEqualTo(400);
});
3. TestPublisher를 이용한 스트림 동작 제어
TestPublisher는 데이터 방출 시점과 오류 발생 시점을 테스트 코드에서 직접 제어할 수 있게 해줍니다. 복잡한 비동기 환경이나 백프레셔 관련 테스트에 적합합니다.
TestPublisher<String> publisher = TestPublisher.create();
Flux<String> targetFlux = publisher.flux();
StepVerifier.create(targetFlux)
.then(() -> {
publisher.next("payload-A");
publisher.error(new RuntimeException("인위적 스트림 중단"));
})
.expectNext("payload-A")
.verifyErrorMessage("인위적 스트림 중단");
4. 오류 복구(Fallback) 로직 검증
onErrorReturn이나 onErrorResume과 같은 연산자가 예외 발생 시 대체 경로를 올바르게 제공하는지 테스트합니다.
Flux<String> resilientStream = Flux.just(5)
.map(val -> {
if (val > 0) throw new ArithmeticException("계산 오류");
return "결과: " + val;
})
.onErrorResume(ArithmeticException.class, e -> Mono.just("기본값 대체"));
StepVerifier.create(resilientStream)
.expectNext("기본값 대체")
.verifyComplete();
5. 재시도(Retry) 메커니즘의 한계 테스트
네트워크 요청 등 재시도 로직이 적용된 스트림에서, 재시도 횟수를 모두 소진했을 때 최종적으로 전파되는 예외를 검증합니다.
Flux<String> retryStream = Flux.<String>error(new TimeoutException("응답 지연"))
.retryWhen(Retry.max(2).transientErrors(true));
StepVerifier.create(retryStream)
.verifyError(TimeoutException.class);
6. 연산자 조합(Transform) 시의 예외 전파
여러 연산자를 하나의 논리적 단위로 묶어 재사용할 때, 해당 파이프라인 내부에서 발생한 예외가 외부로 올바르게 전파되는지 확인합니다.
Function<Flux<String>, Flux<String>> sanitizeAndTransform =
flux -> flux.filter(s -> s != null && !s.isBlank())
.map(String::toLowerCase);
Flux<String> composedStream = Flux.just("HELLO", null, "WORLD")
.transform(sanitizeAndTransform)
.concatWith(Flux.error(new IllegalArgumentException("널 허용 안됨")));
StepVerifier.create(composedStream)
.expectNext("hello")
.verifyError(IllegalArgumentException.class);
7. 핫 스트림(Hot Stream)의 다중 구독자 예외 전파
핫 스트림은 구독자와 무관하게 데이터를 방출하므로, 오류 발생 시 모든 활성 구독자에게 예외가 전파되는지 검증해야 합니다. 최신 Reactor에서는 Sinks API를 사용하는 것이 권장됩니다.
Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
Flux<String> hotStream = sink.asFlux();
StepVerifier.create(hotStream)
.then(() -> sink.tryEmitNext("event-1"))
.expectNext("event-1")
.then(() -> sink.tryEmitError(new RuntimeException("핫 스트림 장애")))
.verifyErrorMessage("핫 스트림 장애");
테스트 작성 시 고려사항
- 가상 시간 활용:
timeout이나delayElements등 시간 기반 연산자의 예외를 테스트할 때는StepVerifier.withVirtualTime()을 사용하여 실제 대기 시간 없이 테스트를 수행합니다. - 부수 효과 검증: 예외 발생 시 데이터베이스 롤백이나 리소스 해제와 같은 사이드 이펙트가 정상적으로 실행되었는지
doOnError나 Mock 객체를 통해 확인합니다. - 경계 조건 테스트: 빈 스트림(
Flux.empty())이나 무한 스트림에서 예외가 발생했을 때의 동작을 반드시 포함합니다.