만베거
egg528
Reactor Debugging

Project Reactor - Debugging

public static void main(String[] args) throws InterruptedException {
 
    String[] fruits = new String[] {"STRAWBERRY", "BANANA", "APPLE", "MELON"};
 
    Flux.fromArray(fruits)
        .map(String::toLowerCase)
        .map(fruit -> fruit.substring(9))
        .map(fruitLastAlphabet -> "과일의 마지막 알파벳은: " + fruitLastAlphabet) // (1)BANANA가 해당 Operator에 입력값으로 들어가면 오류 발생!
        .subscribe(data -> System.out.println(data));
 
    Thread.sleep(50000L);
}
 
// 과일의 마지막 알파벳은: y
// [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
      at java.base/java.lang.String.substring(String.java:1841)
      (생략...)
      at other.Test.main(Test.java:22)
  • Reactor는 기본적으로 선언형 프로그래밍 방식으로 구성되고 비동기적으로 실행되기에 디버깅이 쉽지 않다.
  • 때문에 Reactor에서는 디버깅 환경을 조금이나마 개선하고자 몇 가지 방법을 제공해준다.

1. Debug 모드를 사용한 Debugging

public static void main(String[] args) throws InterruptedException {
    Hooks.onOperatorDebug(); // (1)
 
    String[] fruits = new String[] {"STRAWBERRY", "BANANA", "APPLE", "MELON"};
 
    Flux.fromArray(fruits)
        .map(String::toLowerCase)
        .map(fruit -> fruit.substring(9))
        .map(fruitLastAlphabet -> "과일의 마지막 알파벳은: " + fruitLastAlphabet)
        .subscribe(data -> System.out.println(data));
 
    Thread.sleep(50000L);
}
 
// 과일의 마지막 알파벳은: y
// [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
	at java.base/java.lang.String.substring(String.java:1841)
	Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
// Assembly trace from producer [reactor.core.publisher.FluxMapFuseable] :
	reactor.core.publisher.Flux.map(Flux.java:6517) // (2)
	other.Test.main(Test.java:22) // (3)
// Error has been observed at the following site(s): // (4)
	*__Flux.map ⇢ at other.Test.main(Test.java:22)
	|_ Flux.map ⇢ at other.Test.main(Test.java:23)
// Original Stack Trace:
		at java.base/java.lang.String.substring(String.java:1841)
        (생략...)
		at other.Test.main(Test.java:24)
  • (1)과 같이 Hooks.onOperatorDebug()를 사용해 Debug 모드를 실행할 수 있다.
  • Debug 모드에서는 (2), (3)과 같이 오류가 발생한 Operator와 코드 라인이 추가로 제공된다.
  • 또한 오류 전파 경로를 (4)와 같이 제공해주기 때문에 오류가 어디서 시작되어 어디까지 전파되었는지를 확인할 수 있다.

2. checkpoint()를 사용한 Debugging

public static void main(String[] args) throws InterruptedException {
    String[] fruits = new String[] {"STRAWBERRY", "BANANA", "APPLE", "MELON"};
 
    Flux.fromArray(fruits)
        .map(String::toLowerCase)
        .map(fruit -> fruit.substring(9)) // (1) 오류 발생!
        .map(fruitLastAlphabet -> "과일의 마지막 알파벳은: " + fruitLastAlphabet)
        .checkpoint() // (2)
        .subscribe(data -> System.out.println(data));
 
    Thread.sleep(50000L);
}
 
// 과일의 마지막 알파벳은: y
// [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
	at java.base/java.lang.String.substring(String.java:1841)
	Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
// Assembly trace from producer [reactor.core.publisher.FluxMapFuseable] : (3)
	reactor.core.publisher.Flux.checkpoint(Flux.java:3559)
	other.Test.main(Test.java:24)
// Error has been observed at the following site(s): (4)
	*__checkpoint() ⇢ at other.Test.main(Test.java:24)
// Original Stack Trace:
    at java.base/java.lang.String.substring(String.java:1841)
    at other.Test.lambda$main$0(Test.java:22)
    (생략...)
    at other.Test.main(Test.java:25)
  • Debug 모드는 모든 Operator에서 스택트레이스를 캡처하는 반면 checkpoint()는 특정 Operator 체인 내의 스택 트레이스만 캡처한다.
  • 위 코드와 같이 (1)에서 오류가 발생하였고 (2) 위치까지 오류는 전파될 것이다.
  • 때문에 (3), (4)를 보면 checkpoint() 코드 라인에 오류가 전파되었음을 확인할 수 있다.
  • 단, 위 로그만으로는 정확한 오류 위치를 파악할 수 없기에 checkpoint() 위치를 바꿔가며 확인해보면 좋을 것 같다.
  • 추가로 checkpoint("체크 포인트 1")와 같이 파라미터로 Description을 주어 로그 확인을 용이하게 할 수 있다.

3. log()를 사용한 Debugging

public static void main(String[] args) throws InterruptedException {
 
    String[] fruits = new String[] {"STRAWBERRY", "WATERMELON", "BANANA", "APPLE"};
 
    Flux.fromArray(fruits)
        .map(String::toLowerCase)
        .log()
        .map(fruit -> fruit.substring(9))
        .map(fruitLastAlphabet -> "과일의 마지막 알파벳은: " + fruitLastAlphabet)
 
        .subscribe(data -> System.out.println(data));
 
    Thread.sleep(50000L);
}
 
// [ INFO] (main) | onSubscribe([Fuseable] FluxMapFuseable.MapFuseableSubscriber)
// [ INFO] (main) | request(unbounded)
// [ INFO] (main) | onNext(strawberry)
// 과일의 마지막 알파벳은: y
// [ INFO] (main) | onNext(watermelon)
// 과일의 마지막 알파벳은: n
// [ INFO] (main) | onNext(banana)
// [ INFO] (main) | cancel()
// [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
// Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
	at java.base/java.lang.String.substring(String.java:1841)
	at other.Test.lambda$main$0(Test.java:22)
    (생략...)
	at other.Test.main(Test.java:25)
  • log()는 Reactor Sequence의 동작을 로그로 출력한다.
  • 위 코드를 보면 log() 메서드는 .map(String::toLowerCase) Operator에서 발생한 Signal을 로그로 출력한다.
  • 로그를 통해 "STRAWBERRY"와 "WATERMELON" 값들은 정상 처리되었음을 알 수 있다.
  • 또한 "banana"값이 정상적으로 이후 Operator에 전달되었고 이후 오류가 발생하였음을 추론할 수 있다.