본문 바로가기
트러블 슈팅/BE

RestTemplate으로 외부 API 호출 이후 예외 응답시 body가 유실되는 현상에 대해

by Ramos 2025. 4. 11.

문제 상황

  • Flex Appliance API에 대해 RestTemplate으로 요청 시 예외 응답의 경우 다음과 같이 Flex API에서 내려주는 포멧이 유실되는 문제가 발생함.
    • 특히, 인증관련 API 호출 시 임의로 잘못된 토큰을 토대로 요청했거나 요청 body에 잘못된 값을 기반으로 요청했을 경우 아래 디버깅 화면 처럼 no body로 나타남.

  • 다른 케이스에 대해선 해당 문제가 발생하지 않음. (400 에러)

원인 분석

  • 현재 Flex Appliance 서버의 앞단엔 Nginx가 붙어있는데, 예외 응답의 경우 Nginx에서 gzip 처리로 내려주는 상황에 대해 SimpleClientHttpRequestFactory가 해제하지 못함.
  • 또한, 기본 클라이언트의 내부를 살펴보면 java.net.HttpRetryException으로 예외를 던지기도 하며, org.springframework.web.client.DefaultResponseErrorHandler#getResponseBody 모든 예외를 무시하고 응답 본문이 있더라도 빈 바이트 배열을 반환함.

관련 링크

관련 코드 수정

AS-IS

  • Spring Boot RestTemplate 기본 클라이언트인 SimpleClientHttpRequestFactory를 사용.
  • TLS 관련 부가 설정이 적용됨.
@Slf4j
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    public void checkClientTrusted(X509Certificate[] certs, String authType) {
                    }

                    public void checkServerTrusted(X509Certificate[] certs, String authType) {
                    }
                }
            };
            RestTemplate restTemplate = new RestTemplate();
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

            HostnameVerifier allHostsValid = (hostname, session) -> true;
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);

            // **이 부분에 주목**
            SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
            requestFactory.setConnectTimeout(3 * TimeInMillis.SECOND);
            requestFactory.setReadTimeout(5 * TimeInMillis.SECOND);
            restTemplate.setRequestFactory(requestFactory);

            return restTemplate;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

TO-BE

  • Apache HTTP Component 기반 HttpComponentsClientHttpRequestFactory로 변경
    • build.gradle 내 해당 의존성 추가
  • 기존 TLS 관련 설정에서 모든 인증서를 신뢰하도록 설정함.
    • 변경된 클라이언트의 경우 해당 설정에 대해 JVM 레벨에서 JVM truststore 관련 설정이 필요한데, Real 환경이 특수한 환경(서버간 통신만을 수행 및 인프라 레벨 보안처리가 되어 있는 상황)이라는 점과 추후 인프라 레벨의 변경점을 대비하여 관리 포인트를 줄이는 방향으로 생각하여 Application 레벨에선 별도의 설정을 하지 않도록 처리함.
@Slf4j
@Configuration
public class RestTemplateConfig {

    @Bean
    public HttpClient httpClient() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
        // 모든 인증서를 신뢰하도록 설정한다
        TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
        SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();

        // Https 인증 요청시 호스트네임 유효성 검사를 진행하지 않게 한다.
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);

        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
            .register("https", sslsf)
            .register("http", new PlainConnectionSocketFactory()).build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setConnectionManager(connectionManager);
        return httpClientBuilder.build();

    }

    // 변경된 클라이언트 (Apache HTTP Component)
    @Bean
    public HttpComponentsClientHttpRequestFactory factory(HttpClient httpClient) {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(3000);
        factory.setHttpClient(httpClient);

        return factory;
    }

    @Bean
    public RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }
}

 

이후 결과는 아래와 같음.

 

두 클라이언트를 비교하자면?

주요 차이 상세 설명

  1. 네트워크 처리 기반
  • SimpleClientHttpRequestFactory는 Java SE 표준인 HttpURLConnection 기반
    → 가볍고 의존성 없음, 하지만 기능이 제한됨
  • HttpComponentsClientHttpRequestFactory는 Apache HttpClient 기반
    → 매우 안정적이고 기능 확장성이 크며 대규모 서비스에 적합
  1. GZIP 압축 응답 처리
  • SimpleClientHttpRequestFactory는 GZIP 응답을 직접 처리하지 않음
    ClientHttpRequestInterceptor 또는 스트림 래핑 필요
  • HttpComponentsClientHttpRequestFactoryContent-Encoding: gzip 자동 처리
    → 코드 없이 압축 응답 정상 파싱 가능
  1. SSL 커스터마이징
  • SimpleClientHttpRequestFactory : HttpsURLConnection.setSSLSocketFactory(...) 등으로 설정 필요 → 전역 설정이라 위험
  • HttpComponentsClientHttpRequestFactory : TrustStrategy, NoopHostnameVerifier 등 객체 단위로 안전하게 커스터마이징 가능
  1. 커넥션 관리
  • SimpleClientHttpRequestFactory : 매 요청마다 새로운 TCP 연결
  • HttpComponentsClientHttpRequestFactory : PoolingHttpClientConnectionManager 사용 시 Keep-Alive 및 커넥션 재사용 가능 → 성능 개선, 고부하 처리 유리
  1. 응답 재사용 / 예외 응답 처리
  • SimpleClientHttpRequestFactory : 예외 발생 시 body 스트림이 닫혀서 getResponseBodyAsString() 호출 시 null 또는 빈 문자열
  • HttpComponentsClientHttpRequestFactory: 내부적으로 응답 body를 byte[]로 캐싱해둠 → 재사용 가능

결론

  • SimpleClientHttpRequestFactory는 가볍고 단순한 환경에만 적합.
  • 실전에서는 대부분 HttpComponentsClientHttpRequestFactory를 사용해야 함.
  • Spring Boot에서도 기본 설정을 커스터마이징해서 Apache HttpClient 기반으로 전환하는 것이 모범 사례로 알려짐.

'트러블 슈팅 > BE' 카테고리의 다른 글

Redis 분산락 컴포넌트 리팩토링 적용기  (2) 2025.01.04