문제 발견

개발 중 이상한 현상을 발견했다. Mono 내부에서 예외가 발생했는데, onErrorResume이 동작하지 않는 것이었다.

문제 재현

기본 테스트 코드

Mono<List<String>> getNameList() {
    throw new RuntimeException();
}

테스트 코드:

@Test
void test() {
    getNameList().map(nameList -> {
                    System.out.println(nameList);
                    return nameList;
                })
                .flatMapMany(Flux::fromIterable)
                .collectList()
                .onErrorResume(e -> {
                    System.out.println("error");
                    return Mono.just(List.of("error"));
                })
                .block();
}

// 결과: java.lang.RuntimeException (onErrorResume 동작하지 않음)

문제 분석:

  • Mono 생성 시점에서 바로 예외가 발생하면 Reactive Stream이 시작되기 전에 예외가 던져진다
  • 따라서 onErrorResume 같은 에러 핸들링 연산자가 작동하지 않는다

정상 동작 확인

Mono.error를 사용한 경우

Mono<List<String>> getNameList() {
    return Mono.error(new RuntimeException("error"));
}

// 결과: 정상적으로 "error" 출력

결과 분석:

  • Mono.error()를 사용하면 onErrorResume이 정상적으로 동작한다
  • 이는 예외가 Reactive Stream 내에서 처리되기 때문이다

해결 방법

방법 1: try-catch로 감싸서 Mono.error 반환

Mono<List<String>> getNameList() {
    try {
        throw new RuntimeException();
    } catch (Exception e) {
        return Mono.error(e);
    }
}

// 결과: 정상적으로 "error" 출력

장점: 명시적이고 이해하기 쉽다
단점: 매번 try-catch를 작성해야 한다

방법 2: Mono.defer()로 지연 실행

메서드는 그대로 두고, 호출하는 쪽에서 defer()를 사용한다:

Mono<List<String>> getNameList() {
    throw new RuntimeException();
}
@Test
void test() {
    Mono.defer(() -> getNameList())
        .map(nameList -> {
            System.out.println(nameList);
            return nameList;
        })
        .flatMapMany(Flux::fromIterable)
        .collectList()
        .onErrorResume(e -> {
            System.out.println("error");
            return Mono.just(List.of("error"));
        })
        .block();
}

// 결과: 정상적으로 "error" 출력

왜 defer()가 동작할까?

Mono.defer()지연 실행(lazy evaluation) 을 제공한다:

  1. 구독 시점에 실행: defer() 안의 코드는 실제로 구독이 일어날 때까지 실행되지 않는다
  2. 예외 캐치: 구독 시점에 발생한 예외는 Reactive Stream 내에서 처리된다
  3. 에러 시그널: 예외가 발생하면 자동으로 onError 시그널로 변환된다

결론

문제의 핵심:

  • Mono 생성 시점의 즉시 예외는 Reactive Stream 밖에서 발생한다
  • 따라서 에러 핸들링 연산자들이 동작하지 않는다

권장 해결방법:

  1. 가능한 한 Mono.error() 사용
  2. 불가피한 경우 Mono.defer() 활용
  3. 복잡한 로직은 try-catch로 명시적 처리

Reactive Programming에서는 예외도 스트림의 일부로 다뤄져야 한다는 점을 기억하자!