[Project Reactor] 12. Debugging

y001·2025년 5월 29일

Reactive Programming

목록 보기
12/30
post-thumbnail

Reactor 기반의 리액티브 프로그래밍에서 발생하는 오류는 일반적인 스택트레이스로는 원인을 추적하기 어려운 경우가 많다. 특히 연산자 체인이 복잡해질수록 에러의 지점을 정확히 파악하기 어려워 디버깅이 힘들 수 있다. 이러한 상황을 보완하기 위해 Reactor는 디버깅을 위한 여러 도구들을 제공한다. 본 글에서는 Reactor의 대표적인 디버깅 도구 세 가지를 정리한다:


1. 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);
    }
}

장점

  • 추적 범위가 넓음: 애플리케이션 전체에서 발생하는 오류를 포착할 수 있다.
  • 명확한 원인 파악: 에러 발생 지점뿐만 아니라 그에 이르는 연산자 체인을 확인할 수 있다.

단점

  • 성능 이슈: 모든 연산자 선언 시점의 정보를 보관하기 때문에 메모리 사용량과 성능에 영향을 줄 수 있다.
  • 운영 환경에서는 비권장: 디버깅 환경에서만 사용하는 것을 추천한다.

주의사항

  • 반드시 연산자 선언 전에 호출되어야 한다.
  • 설정 순서가 뒤바뀌면 디버깅 정보가 캡처되지 않는다.

2. 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 함께 출력.

장점

  • 성능에 영향을 거의 주지 않음: 필요한 곳에만 trace 설정 가능.
  • 선택적 추적 가능: 에러 발생 지점을 분리하여 단계별 추적 가능.

단점

  • 단일 checkpoint()만으로는 정확한 위치 파악이 어려울 수 있음.
    → 필요에 따라 여러 위치에 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);

3. 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)**을 설정할 수 있다. (e.g., 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-tools
  • Spring Boot에서는 기본적으로 spring.reactor.debug-agent.enabled=true
  • 수동 설정 시 ReactorDebugAgent.init() 호출

마무리: 어떤 방식이 언제 적합한가?

디버깅 도구특징적합한 상황
Hooks.onOperatorDebug()전체 흐름 추적, 가장 강력함개발 초기, 전체 추적 필요할 때
checkpoint()국소적 추적, 식별자 삽입 가능특정 지점만 추적하고 싶을 때
log()시그널 흐름 파악, 단계별 이벤트 추적흐름이나 시퀀스 디버깅 시
ReactorDebugAgent운영 환경에서도 사용 가능한 Agent프로덕션에서 Assembly 트레이스 필요 시

0개의 댓글