Reactor Core 예외 처리 및 장애 시나리오 테스트 가이드

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())이나 무한 스트림에서 예외가 발생했을 때의 동작을 반드시 포함합니다.

태그: Reactor Core Project Reactor java reactive programming StepVerifier

7월 24일 02:21에 게시됨