Technology/Spring

WebClient, onErrorResume 을 통한 콜백 구조 개선하기!

ikjo 2024. 2. 5. 04:14

콜백 안에 또 다른 콜백

spring webflux 에서 제공하는 WebClient 를 기반으로 3rd party server 와 통신하면서 retry 처리를 해줘야 할 일이 있었다.

 

여기서 말하는 retry 처리란 아래와 같았다.

 

1. our server 는 target server 에 요청 시 access token 을 함께 전송한다.

2. target server 는 our server 의 요청을 처리하기 전 access token 이 유효한지 검증한다.

3. (access token 이 만료된 경우) target server 는 our server 에 access token 이 만료되었다고 응답한다.

4. (해당 응답을 받은) our server 는 target server 에 새로운 access token 발급을 요청한다.

5. target server 는 새로운 access token 을 발급하고 응답한다.

6. our server 는 응답받은 새로운 access token 과 함께 앞선 요청을 재요청한다.

 

일부 과정이 생략되긴 했는데, retry 처리는 대략적으로 위와 같았다. 이때, WebClient 를 통한 3rd party server(target server) 와의 통신 과정은 non-blocking I/O 로 이루어지고 있었다. 이에, our server 는 target server 로 전송한 요청이 정상 처리 또는 실패 응답을 받았을 때, 각각의 처리는 모두 callback 함수를 통해 이루어지게 된다.

 

위와 같은 retry 처리를 순수하게 callback 만을 이용했을 때 코드의 구조는 대략 아래와 같은 형태를 띄었다.

 

    String accessToken = "accessToken";
	Mono<Response> mono = sendRequest(accessToken);

    mono.subscribe(
    	response -> {
            // successfully process
        },
        throwable -> {
            // failed to process
            
            if (invalidAccessToken) {
                String renewAccessToken = getRenewAccessToken();

                Mono<Response> retryMono = sendRequest(renewAccessToken);

                retryMono.subscribe(
                    retryResponse -> {
                        // successfully process
                    },
                    retryThrowable -> {
                        // failed to process
                    }
                );
            }
        }
    );

 

흔히 인터넷 상에서 '아도겐 짤'로도 유명한 콜백 지옥(callback hell)의 기운이 나는 코드였다. 😅 사실, 아래 짤처럼 depth 가 깊은 것은 아니지만, 콜백 안에 또 다른 콜백이 있는 구조가 가독성 상 좋은 구조는 아니라고 생각했다. (사실, 가독성이라 함은 개개인 주관에 따라 상이한 부분이긴 하지만, 상당수의 프로그래밍 관련 각종 국내외 사설에서는 중첩된 콜백 형태는 가독성이 좋지 않다고 보는 편이다.)

 

 

Mono 에서는 retryWhen 등의 기능을 제공해주긴 하나, 현 상황에서는 단순히 재요청하는 것이 아니라 access token 이 만료되었다는 응답을 받았을 때 access token 을 재발급받은 후 해당 access token 으로 다시 요청을 보내야했기에, 해당 기능을 활용하기는 어려웠다.

 

 

onErrorResume 를 통한 콜백 구조 개선

Mono 에서는 retryWhen 외에도 다양한 기능을 제공해주는데, 위와 같은 상황에서 콜백 지옥을 개선해줄만한 기능으로 onErrorResume 이 있었다. 이때, onErrorResume 은 에러가 발생했을 때 (target server 가 에러 응답을 보냈을 때) 새로운 시퀀스(Mono)로 대체하고 싶은 경우 사용된다.

 

기존 코드에서 onErrorResume 을 도입함으로써 대략 아래와 같이 코드 구조를 개선시켜볼 수 있었다.

 

    String accessToken = "accessToken";
    Mono<Response> mono = sendRequest(accessToken);

    mono.onErrorResume(
    		throwable -> throwable instanceof ExpiredAccessTokenException,
            throwable -> {
                String renewAccessToken = getRenewAccessToken();
                return sendRequest(renewAccessToken);
            }
    	)
    .subscribe(
    	response -> {
            // successfully process
        },
        throwable -> {
            // failed to process
        }
    );

 

기존 콜백 안에 또 다른 콜백이 있는 형태에서 onErrorResume 을 적용해 메서드 체이닝 형태로 개선된 것을 볼 수 있다. 메서드 체이닝 형태로 바뀌면서 반복적인 코드 작성도 줄었을 뿐만 아니라 가독성도 향상되었다. 참고로, 앞서 onErrorResume 은 에러 발생 시 새로운 시퀀스로 대체해준다고 했는데, 여기서는 에러(access token 이 만료되었을 경우 발생하는 에러) 발생 시 sendRequest(renewAccessToken) 이 반환하는 시퀀스(Mono)가 새로운 시퀀스가 되는 것이다.