
Reactor 기반의 리액티브 프로그래밍에서 발생하는 오류는 일반적인 스택트레이스로는 원인을 추적하기 어려운 경우가 많다. 특히 연산자 체인이 복잡해질수록 에러의 지점을 정확히 파악하기 어려워 디버깅이 힘들 수 있다. 이러한 상황을 보완하기 위해 Reactor는 디버깅을 위한 여러 도구들을 제공한다. 본 글에서는 Reactor의 대표적인 디버깅 도구 세 가지를 정리한다:
Hooks.onOperatorDebug() – 전역(Global) 디버깅 활성화Hooks.onOperatorDebug()는 애플리케이션 전체의 Flux/Mono 연산자 체인에서 발생하는 오류에 대해 Assembly Stacktrace를 자동으로 캡처해준다. 이 Stacktrace는 예외 발생 시 **기존 예외 스택 중간에 삽입(suppressed)**되어 출력된다.
public class DebugModeExample {
public static void main(String[] args) {
Hooks.onOperatorDebug(); // 전체 연산자 체인에 대한 디버깅 활성화
Flux.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x/y) // 0으로 나누는 예외 발생
.subscribe(System.out::println, Throwable::printStackTrace);
}
}
checkpoint() – 로컬(Local) 단위 디버깅checkpoint()는 전역 디버깅과는 달리 특정 위치에서만 스택트레이스를 캡처할 수 있다. 특히 에러가 발생할 가능성이 높은 연산자 체인에 국한해서 사용하는 것이 유효하다.
Flux.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x / y)
.checkpoint("ZipWith Checkpoint", true) // 여기만 traceback 캡처
.map(num -> num + 2)
.subscribe(System.out::println, Throwable::printStackTrace);
checkpoint("description"): 설명만 출력. 스택트레이스는 캡처하지 않음.checkpoint("description", true): 설명 + traceback 함께 출력.checkpoint()만으로는 정확한 위치 파악이 어려울 수 있음.checkpoint()를 추가해 전파 과정을 분석해야 함.Flux.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x / y)
.checkpoint("zipWith Checkpoint", true)
.map(num -> num + 2)
.checkpoint("map Checkpoint", true)
.subscribe(System.out::println, Throwable::printStackTrace);
log() – 시퀀스 이벤트 시각화log()는 Reactor Sequence 내에서 발생하는 **시그널 이벤트(onSubscribe, onNext, onError, onComplete 등)**를 로그로 출력한다. 시퀀스의 흐름을 시각적으로 추적하고 싶은 경우 매우 유용하다.
Flux.just("A", "B", "C")
.log("MyLog", Level.INFO)
.map(String::toLowerCase)
.subscribe(System.out::println, Throwable::printStackTrace);
Level.INFO, Level.FINE, Level.FINEST)log("tagName"))를 붙여서 여러 log 오퍼레이터를 구분 가능Hooks.onOperatorDebug()와 병행 사용 시, traceback도 함께 출력 가능Flux.fromArray(new String[]{"BANANAS", "APPLES", "PEARS", "MELONS"})
.log("Fruit.Source")
.map(String::toLowerCase)
.log("Fruit.Lower")
.map(fruit -> fruit.substring(0, fruit.length() - 1))
.log("Fruit.Substring")
.map(fruits::get)
.log("Fruit.Name")
.subscribe(Logger::onNext, Logger::onError);
출력 예시 (일부):
INFO Fruit.Source -- onNext(BANANAS)
INFO Fruit.Lower -- onNext(bananas)
INFO Fruit.Substring -- onNext(banana)
INFO Fruit.Name -- onNext(바나나)
...
ERROR Fruit.Name -- | onError(java.lang.NullPointerException: ...)
ReactorDebugAgent운영 환경에서는 Hooks.onOperatorDebug() 대신 ReactorDebugAgent 사용을 권장한다. 이는 Java Agent로 동작하며, 별도의 성능 오버헤드 없이 assembly 정보를 주입할 수 있다.
reactor-toolsspring.reactor.debug-agent.enabled=trueReactorDebugAgent.init() 호출| 디버깅 도구 | 특징 | 적합한 상황 |
|---|---|---|
Hooks.onOperatorDebug() | 전체 흐름 추적, 가장 강력함 | 개발 초기, 전체 추적 필요할 때 |
checkpoint() | 국소적 추적, 식별자 삽입 가능 | 특정 지점만 추적하고 싶을 때 |
log() | 시그널 흐름 파악, 단계별 이벤트 추적 | 흐름이나 시퀀스 디버깅 시 |
ReactorDebugAgent | 운영 환경에서도 사용 가능한 Agent | 프로덕션에서 Assembly 트레이스 필요 시 |