Day 4. SpringBoot -> FastAPI : Http Interface

2ㅣ2ㅣ·2024년 10월 25일

Project

목록 보기
3/13

개요

사용자가 일기를 작성하면 SpringBoot에서 Chat 봇의 응답을 리턴해줄 FastAPI의 API를 호출해야한다.

🫨 As-Is

처음엔 WebClient을 사용했다.
RestTemplate가 Deprecated 된다는 소문도 있어서 WebClient를 사용해야겠구나 생각했다.
더군다나 비동기 논블로킹 방식을 사용한다고 하니 동시성 측면에서도 유리하다고 생각했다.

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

FastAPI Call 할 서비스단

public BaseResponse<SongResponseDto> getChat(SongRequestDto songRequestDto){
        SongResponseDto songResponseDto =  webClient.post()
                .uri("http://localhost:8000/chat")
                .bodyValue(songRequestDto)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError,
                        clientResponse -> Mono.error(new CustomException(BaseResponseStatus.INTERNAL_CLIENT_ERROR)))
                .onStatus(HttpStatusCode::is5xxServerError,
                        clientResponse -> Mono.error(new CustomException(BaseResponseStatus.INTERNAL_SERVER_ERROR)))
                .bodyToMono(SongResponseDto.class)
                .block();

        return new BaseResponse<>(songResponseDto);
    }
  • uri에 호출할 FastAPI의 URI를 적었다.
    • dashboard 페이지에서 chat을 호출하는 것이므로 Restful하게 적었음
  • Status를 4xx과 5xx로 나누어 각 경우에 맞는 CustomException을 던졌다.
    • 프론트 개발도 내가 전담하기 때문에 에러 유형을 최대한 알기 쉽게 처리했음

다만, WebClient는 MVC 기반으로 진행하고 있는 우리 프로젝트 디자인 패턴에 맞지 않는다는 단점이 있다.
WebClient는 비동기 논블로킹 방식으로 하나의 스레드가 여러 개의 요청을 처리할 수 있어 다수의 스레드가 빠른 동시성을 유지할 수 있지만, Spring MVC는 하나의 요청이 들어오면 하나의 스레드가 모든 작업을 처리하는 것을 권장하기 때문이다.

즉, 동시성이 우선이냐 디자인 패턴이 우선이냐를 선택해야 한다는 건데...

block()을 사용하여 동기 처리를 해주면 되지만 WebClient의 장점을 퇴색시키는 것 같아 찜찜했다.

💡 To-Be

관련 자료를 더 찾아보던 중, RestClient의 존재를 알게되었다!!
RestTemplate의 동기 방식 + WebClient의 메서드체이닝 방식과 커스텀 오류 방식을 합친 HTTP Client이다.
WebClient 에 비해 동시성은 떨어질지 언정, 동기 방식이기 때문에 Spring MVC에 위반되지 않는다는 점에서 우리 프로젝트에 적합한 방식이라는 생각이 들었다.


build.gradle

implementation 'org.springframework.boot:spring-boot-starter-web' // 기존 Spring 동기 방식 유지
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1' // Http 요청시 타임 아웃, 연결 관리 등의 기능 제공

시도 1

RestClient를 사용하기 위한 @Configuration을 정의했다.

@Configuration
public class RestClientConfig {

    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(5000);
        return new RestTemplate(factory);
    }

    @Bean
    public RestClient restClient(RestTemplate restTemplate) {
        return RestClient.create(restTemplate);
    }
}

문제 상황 : Cannot resolve method 'setReadTimeout' in 'HttpComponentsClientHttpRequestFactory'
-> Spring Boot 3.x에서는 Apache HttpClient 버전이 변경되면서 HttpComponentsClientHttpRequestFactory의 setReadTime 같은 메서드를 사용할 수 없게 됐다.

해결 방법
때문에 HttpComponentsClientHttpRequestFactory 대신 SimpleClientHttpRequestFactory를 사용하거나 Apache HttpClient의 최신 버전을 주입해야 했다.

시도 2

변경 사항 : Apache HttpClient의 최신 버전을 주입하는 방향으로 시도해보았다.

buiild.gradle

'org.apache.httpcomponents.client5:httpclient5:5.2.1'
  • Spring Boot 3.x에서 Spring Framework 6.0 이상을 사용할 때 Apache HttpClient 5.x를 사용해야 한다.

RestClientConfig.java
Apache HttpClient 5.x를 사용하기 위해 HttpClientConnectionManager를 사용했다.

@Configuration
public class RestClientConfig {

    private static final int READ_TIMEOUT = 1500;
    private static final int CONNECT_TIMEOUT = 3000;

    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setHttpClient(createHttpClient());
        return new RestTemplate(factory);
    }

    @Bean
    public RestClient restClient(RestTemplate restTemplate) {
        return RestClient.create(restTemplate);
    }

connection pooling and timeouts
    private CloseableHttpClient createHttpClient() {
        return HttpClients.custom()
                .setConnectionManager(createHttpClientConnectionManager())
                .build();
    }

    private PoolingHttpClientConnectionManager createHttpClientConnectionManager() {
        return PoolingHttpClientConnectionManagerBuilder.create()
                .setDefaultConnectionConfig(ConnectionConfig.custom()
                        .setSocketTimeout(TimeValue.ofMilliseconds(READ_TIMEOUT))
                        .setConnectTimeout(TimeValue.ofMilliseconds(CONNECT_TIMEOUT))
                        .build())
                .build();
    }
}

문제 상황 : 역시나 Apache HttpClient 버전 업그레이드로 인한 오류가 발생했다. PoolingHttpClientConnectionManager 에서 setDefaultConnectionConfig() 메서드 내부 설정이 변경되어 직접 메서드를 호출하여 타임아웃 설정을 못하게 되었다고 한다.

해결 방법 : PoolingHttpClientConnectionManager 내부의 타임아웃 설정을 제거하고, RequestConfig에서 처리하도록 변경했다.

시도 3

변경 사항 : RequestConfig를 사용해 타임아웃 설정을 createHttpClient 메서드 내에서 처리하고, 이를 HttpClient에 주입했다.
또, TimeValue 대신 Timeout 객체를 사용했다.

@Configuration
public class RestClientConfig {

    private static final int READ_TIMEOUT = 1500;
    private static final int CONNECT_TIMEOUT = 3000;

    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setHttpClient(createHttpClient());
        return new RestTemplate(factory);
    }

    // HttpClient 생성 메서드
    private CloseableHttpClient createHttpClient() {
        RequestConfig config = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.ofMilliseconds(CONNECT_TIMEOUT))
                .setResponseTimeout(Timeout.ofMilliseconds(READ_TIMEOUT))
                .build();

        // HttpClient 생성 및 설정
        return HttpClients.custom()
                .setDefaultRequestConfig(config)
                .setConnectionManager(new PoolingHttpClientConnectionManager())
                .build();
    }

    @Bean
    public RestClient restClient(RestTemplate restTemplate) {
        return RestClient.create(restTemplate);
    }

FastAPI Call 할 서비스단
RestClient는 WebClient의 메서드 체이닝과 커스텀 에러 방식을 유지하기 때문에 문법적으로 크게 바뀌지 않았다.

public BaseResponse<SongResponseDto> getChat(SongRequestDto songRequestDto){
        String url = "통신할 FastAPI url";

        ResponseEntity<SongResponseDto> songResponseDto = restClient.post()
                .uri(url)
                .body(songRequestDto)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
                    throw new CustomException(BaseResponseStatus.INTERNAL_CLIENT_ERROR);
                })
                .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
                    throw new CustomException(BaseResponseStatus.SERVER_ERROR);
                })
                .toEntity(SongResponseDto.class);

        return new BaseResponse<>(songResponseDto.getBody());
    }

일단 컴파일 및 런타임 에러는 발생하지 않았다.. 추후 클라우드에 띄운 FastAPI 서버와 통신을 해본 뒤 변경 사항이 생기지 않을까 싶다 😒


참고
✔️ RestTemaplte 알아보기
✔️ Apache HttpClient 의존성 문제

0개의 댓글