본문 바로가기
Java

Mono.defer

by ybs 2021. 11. 11.
반응형

스택오버플로 글을 보면 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);
}
반응형