Mono.defer
스택오버플로 글을 보면 Mono.defer 는 defer 안 코드를, 선언 시점이 아닌 실행 시점에 동작하게 해준다. 그래서 나는 한 context에서 여러번 subscribe 할 때 필요하겠구나로 이해했다. 하지만 그렇지 않은 상황에서도 defer 를 만나게 됐고 처음엔 왜 사용했는지 이해가 안됐다.
이해를 위해 아래 코드를 보자. id를 파라미터로 받아 Data를 얻어오는 메서드다. defer를 사용 안한다면 아래와 같이 switchIfEmpty 에 넣을 Mono를 따로 선언해야된다. 그래서 캐시에 값이 있으면 switchIfEmpty 를 실행 안시킴에도 항상 this.apiClient.getDataNoById(id) 메서드가 수행된다.
// Mono.defer 사용 안한 코드
public Mono<Data> requiredById(String id) {
Mono<Long> alternate = this.apiClient.getDataNoById(id).doOnSuccess(TODO);
return Mono.justOrEmpty(cache.getData(id))
.switchIfEmpty(alternate)
.flatMap(this::requiredOne)
...
}
cf) alternate 변수로 따로 빼지 않고 아래 코드처럼 switchIfEmpty 인자 위치에 메서드 호출하면 되지 않을까 생각할 수 있지만 cahe.getData(id) 에 값이 있어도 this.apiClient.getDataNoById(id) 는 항상 호출된다(글 마지막에 테스트 코드 설명).
// Mono.defer 사용 안한 코드
public Mono<Data> requiredById(String id) {
return Mono.justOrEmpty(cache.getData(id))
.switchIfEmpty(this.apiClient.getDataNoById(id).doOnSuccess(TODO))
.flatMap(this::requiredOne)
...
}
Mono.defer 를 사용하면 switchIfEmpty 가 실행될 때 동작하게 미룰 수 있다. 그런데 apiClient.getDataNoById() 메서드는 webClient 를 호출해 Mono 타입을 리턴 받는다. 바로 block 메서드를 써서 subscribe 하지 않기 때문에 실제 webClient 호출은 발생하지 않는다. 그래서 defer를 왜 사용했는지 이해를 못했다. 결국 switchIfEmpty 가 람다 표현식을 지원하면 defer를 안써도 되는 문제였다. 하지만 자바에서는 지원하지 않았기 때문에 defer를 이용했다.
// Mono.defer 사용한 코드
public Mono<Data> requiredById(String id) {
return Mono.justOrEmpty(cache.getData(id))
.switchIfEmpty(Mono.defer(() -> this.apiClient.getDataNoById(id).doOnSuccess(TODO)))
.flatMap(this::requiredOne)
...
}
마지막으로 spring.web.reactive.DispatcherHandler 에서도 Mono.defer 를 사용한것을 볼 수 있다.
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
private <R> Mono<R> createNotFoundError() {
return Mono.defer(() -> {
Exception ex = new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler");
return Mono.error(ex);
});
}
단순히 Exception 객체 하나 만들고 Mono.error 로 감싸는 것인데도 defer를 사용한것을 보면 깔끔한 코드를 만들기 위한 방법으로 쓴거 같다.
switchIfEmpty 테스트
monoMethod1 에서 new Object() 를 넣어서 항상 switchIfEmpty 가 수행안되게 했지만
@Test
void switchIfEmptyTest() {
System.out.println(monoMethod1().block());
}
private Mono<Object> monoMethod1() {
return Mono.justOrEmpty(new Object())
.switchIfEmpty(getValue().doOnSuccess(it -> System.out.println("doOnSucess : " + it)));
}
private Mono<String> getValue() {
System.out.println("getValue Executed!");
return Mono.just("plain mono result");
}
출력 결과를 보면 항상 getValue() 메서드가 호출된다. cf) doOnSuccess 는 수행안됌.
21:46:14.172 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
getValue Executed!
java.lang.Object@18078bef
Process finished with exit code 0
하지만 defer 는 new Object() 값을 넣으면 람다 바디안 로직을 수행안한다.
private Mono<Object> deferMethod() {
Mono<String> defer = Mono.defer(() -> {
System.out.println("Deferred Mono exec");
return Mono.just("deferred mono result");
});
return Mono.justOrEmpty(new Object())
.switchIfEmpty(defer);
}