Ractor에서의 디버깅 방법
- 동기식 또는 명령형 프로그래밍 방식은 Exception이 발생 했을 때
- Stacktrace를 확인하거나 예외 발생이 예상되는 코드에 Breakpoint를 걸어서 문제가 발생한 원인을 단계적으로 찾아가면 되기 때문에, 상대적으로 디버깅이 쉽다.
Hooks.onOperatorDebug()
를 통해서 Reactor에서의 디버그 모드 활성화 가능
ReactorDebugAgent
를 사용하여 프로덕션 환경에서 디버그 모드 활성화 가능
1. Hooks.onOperatorDebug()
- Application 내에 있는 모든 Operator의 Stacktrace를 캡처한다.
- Error가 발생하면 캡처한 정보를 기반으로 Error가 발생한 Assembly의 Stacktrace를 원본 Stacktrace 중간에 끼워 넣는다.
public static Map<String, String> fruits = new HashMap<>();
static {
fruits.put("banana", "바나나");
fruits.put("apple", "사과");
fruits.put("pear", "배");
fruits.put("grape", "포도");
}
public static void main(String[] args) throws InterruptedException {
Hooks.onOperatorDebug();
Flux
.fromArray(new String[]{"BANANAS", "APPLES", "PEARS", "MELONS"})
.subscribeOn(Schedulers.boundedElastic())
.publishOn(Schedulers.parallel())
.map(String::toLowerCase)
.map(fruit -> fruit.substring(0, fruit.length() - 1))
.map(fruits::get)
.map(translated -> "맛있는 " + translated)
.subscribe(
log::info,
error -> log.error("# onError:", error));
Thread.sleep(100L);
}
2. ReactorDebugAgent
- Reactor Tool에서 지원하는
ReactorDebugAgent
를 사용하여 프로덕션 환경에서 디버그 모드를 대체할 수 있다.
Assembly
- Operator에서 리턴하는 새로운 Mono 또는 Flux가 선언된 지점을 Assembly라고 한다.
Traceback
- 디버그 모드를 활성화하면 Operator의 Assembly정보를 캡처하는데 이중에서 에러가 발생한 Operator의 Stacktrace를 캡처한 Assembly 정보를 Traceback이라고 한다.
- Traceback은 Suppressed Exception 형태로 원본 Stacktrace에 추가된다.
- Suppressed Exceptions Java Suppressed Exceptions | Baeldung
Production 환경에서의 디버깅 설정
- Reactor에서는 Application 내 모든 Operator 체인의 Stacktrace 캡처 비용을 지불하지 않고 디버깅 정보를 추가할 수 있도록 별도의 Java Agent를 제공합니다.
- 아래와 같이 2가지를 설정하면,
ReactorDebugAgent
를 활성화할 수 있다.
implementation("io.projectreactor:reactor-tools:3.5.8")
spring.reactor.debug-agent.enabled: true
ReactorDebugAgent.init()
- 자동 실행 되거나, 혹은 직접 호출해주어야 한다.
checkpoint() Operator를 사용한 디버깅
Hooks.onOperatorDebug()
을 통한 디버그 모드를 활성화하는 방법이 Application 내에 있는 모든 Operator에서 Stacktrace를 캡처하는 반면에,
checkpoint()
Operator를 사용하면 특정 Operator 체인 내의 Stacktrace만 캡처한다.
1. Traceback을 출력하는 방법
checkpoint()
를 사용하면 실제 에러가 발생한 assembly 지점 또는 에러가 전파된 assembly 지점의 traceback이 추가된다.
Flux
.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x/y)
.map(num -> num + 2)
.checkpoint()
.subscribe(
data -> log.info("# onNext: {}", data),
error -> log.error("# onError:", error)
);
Flux
.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x/y)
.checkpoint()
.map(num -> num + 2)
.checkpoint()
.subscribe(
data -> log.info("# onNext: {}", data),
error -> log.error("# onError:", error)
);
2. Traceback 출력 없이 식별자를 포함한 Description을 출력해서 에러 발생 지점을 예상하는 방법
checkpoint(description)
을 사용하면 에러 발생 시 Traceback을 생략하고 description을 통해 에러 발생 지점을 예상할 수 있다.
Flux
.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x/y)
.checkpoint("Example12_4.zipWith.checkpoint")
.map(num -> num + 2)
.checkpoint("Example12_4.map.checkpoint")
.subscribe(
data -> log.info("# onNext: {}", data),
error -> log.error("# onError:", error)
);
3. Traceback과 Description을 모두 출력하는 방법
checkpoint(description, forceStackTrace)
를 사용하면 description과 Traceback을 모두 출력할 수 있다.
Flux
.just(2, 4, 6, 8)
.zipWith(Flux.just(1, 2, 3, 0), (x, y) -> x/y)
.checkpoint("Example12_4.zipWith.checkpoint", true)
.map(num -> num + 2)
.checkpoint("Example12_4.map.checkpoint", true)
.subscribe(
data -> log.info("# onNext: {}", data),
error -> log.error("# onError:", error)
);
4. log() Operator를 사용한 디버깅
log()
Operator는 Reactor Sequence의 동작을 로그로 출력한다.
@Slf4j
Map<String, String> fruits = Map.of(
"banana", "바나나",
"apple", "사과",
"pear", "배",
"grape", "포도"
);
Flux.fromArray(new String[]{"BANANAS", "APPLES", "PEARS", "MELONS"})
.map(String::toLowerCase)
.map(fruit -> fruit.substring(0, fruit.length() - 1))
.log()
.map(fruits::get)
.subscribe(
log::info,
error -> log.error("# onError:", error));