WebClient 을 활용한 알림톡 비동기 처리 및 성능 개선

만약, 비즈니스 로직에서 무조건 순서대로 처리되어야 할 필요가 없는 HTTP 요청이 있다면 이를 비동기 방식으로 변경하는 것을 고려해볼 수 있을 것이다.

그렇게 변경을 하게 된다면 HTTP 요청과 관계 없이 비즈니스 로직은 수행될 것이고 사용자는 불필요한 대기 없이 결과를 받을 수 있을 것이다.

대학원 김선배의 서비스에서는 몇가지 경우에 사용자에게 알림톡을 전송하는 경우가 있다.

이를 위해서는 알림톡 대행사의 서버에 HTTP 요청을 보내야 하는데, 여기서 동기 방식의 HTTP 요청을 보내게 된다면 알림톡 대행사의 서버에서 동작을 완료하고 응답을 해주어야 다음 로직을 수행하고 사용자에게 응답을 할 수 있다.

하지만 이러한 경우 사용자는 불필요한 대기 시간을 경험할 것이다.

심지어 생각보다 긴 시간을 대기해야 하기에 사용자에게 안좋은 경험을 제공하게 된다.

코드 작성

우선, 이를 위해서 WebClient 설정을 진행하고 코드를 작성해보자.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

// webflux 의존성 추가
@Configuration
public class WebClientConfig {

    @Bean
    public ReactorResourceFactory resourceFactory() {
        return new ReactorResourceFactory();
    }

    @Bean
    public WebClient webClient() {
        Function<HttpClient, HttpClient> mapper = client -> HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10))
                        .addHandlerLast(new WriteTimeoutHandler(10)))
                .responseTimeout(Duration.ofSeconds(10));

        ClientHttpConnector connector =
                new ReactorClientHttpConnector(resourceFactory(), mapper);
        return WebClient.builder().clientConnector(connector).build();
    }
}

이러한 설정을 통해 타임아웃과 같은 기본적인 설정을 진행한다.

그리고 WebClient를 이용하여 외부 API에 요청을 보내는 내용을 아래와 같이 작성할 수 있다.

@RequiredArgsConstructor
@Slf4j
@Component
public class BizppurioSend {
    private final BizppurioAuth bizppurioAuth;
    private final ObjectMapper objectMapper;
    private final WebClient webClient;

    @Value("${bizppurio.message}")
    private String messageUrl;

    protected void sendMessageWithExceptionHandling(Supplier<CommonRequest> messageSupplier) {
        try {
            CommonRequest commonRequest = messageSupplier.get();
            String accessToken = bizppurioAuth.getAuth();
            String request = objectMapper.writeValueAsString(commonRequest);
            webClient.post()
                    .uri(messageUrl)
                    .headers(h -> h.setContentType(APPLICATION_JSON))
                    .headers(h -> h.setBearerAuth(accessToken))
                    .bodyValue(request)
                    .retrieve()
                    .bodyToMono(MessageResponse.class)
                    .subscribe(this::check);
        } catch (Exception ex) {
            log.error("알림톡 전송 예외 발생: {}", ex.getMessage());
        }
    }

    private void check(MessageResponse response) {
        if (response.code() != 1000) {
            log.error("전송실패 errorCode : {} errorMessage : {}", response.code(), response.description());
            return;
        }
        log.info("알림톡 전송에 성공하였습니다.");
    }
}

이때 기존의 RestTemplate과 다른 특징은 .subscribe() 에서 나타난다.

int result = webClient.get().uri("/test1")
                .retrieve()
                .bodyToMono(Integer.class)
                .block();
                
List<Integer> resultList = webClient.get().uri("/test2")
                .retrieve()
                .bodyToFlux(Integer.class)
                .toStream()
                .collect(Collectors.toList());

만약 이러한 방식으로 .block() 을 사용한다면 동기 방식을 사용하겠다는 것으로 RestTemplate과 사실상 차이가 없다.

즉, subscribe() 가 비동기 방식을 사용한데 있어 핵심적인 부분인 점을 기억하고 코드를 전체적으로 살펴보면 코드 자체는 해석하는데 어려움이 없을 것이다.

subscribe() 는 publish, subscribe 메시징 방식을 생각하면 비슷한데, 요청에 대한 응답이 돌아오면 동작할 코드를 작성하는 곳이다.

나의 경우는 리턴 값이 존재하지 않기 때문에 응답 코드에 따라 로그를 찍도록 처리하고 있다.