개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Spring › 그 밖의 기능

외부 API 호출과 타임아웃

다른 서버를 호출했는데 답이 오지 않으면 언제까지 기다리나

주문 서버가 결제 서버의 API를 호출합니다. 호출한 스레드는 결과가 올 때까지 멈춰 서서 기다리는데, 얼마나 기다리다 포기할지를 정하는 것이 타임아웃입니다. 타임아웃은 두 가지입니다. 연결 타임아웃은 연결이 맺어지기까지, 읽기 타임아웃은 연결된 뒤 데이터가 오지 않은 채 기다리는 시간의 한도입니다. 정하지 않으면 클라이언트는 스스로 포기하지 않습니다. 상대 서버의 상태와 타임아웃을 바꿔 가며 어느 타임아웃이 언제 걸리는지 보세요. 화면에 나오는 2초, 3초, 10초는 예시 값입니다.

일부만 실행으로 확인 · 스프링 7.0.9의 RestClient로 스물네 가지 조합을 실제 호출해 대조. 답이 없는 연결 네 가지는 네트워크 환경에 따라 다를 수 있음

이 실습에 나오는 개념

연결 타임아웃과 읽기 타임아웃
다른 서버를 호출할 때 얼마나 기다리다 포기할지를 정하는 두 가지 시간이다. 연결 타임아웃은 상대와 TCP 연결이 맺어지기까지 기다리는 한도이고, 읽기 타임아웃은 연결이 맺어진 뒤 응답 데이터가 오지 않는 채로 기다리는 한도다. 두 시간은 서로 다른 구간을 재므로, 연결 타임아웃만 정하면 연결은 됐는데 응답이 늦는 경우를 막지 못한다. 타임아웃을 정하지 않으면 느린 상대 때문에 호출한 스레드가 계속 묶이고, 스레드 풀이 바닥나면 장애가 호출한 쪽으로 번진다. 읽기 타임아웃이 났다고 해서 상대가 요청을 처리하지 않았다는 뜻은 아니므로, 같은 요청을 다시 보낼 때는 중복 처리를 조심해야 한다.
RestClient
스프링에서 다른 서버의 HTTP API를 호출할 때 쓰는 클라이언트다. client.get().uri(주소).retrieve().body(타입)처럼 메서드를 이어 쓰는 방식으로 요청을 만들고, 응답 본문은 메시지 컨버터가 객체로 바꿔 돌려준다. 응답이 올 때까지 호출한 스레드가 기다리는 동기 방식이며, 예전부터 쓰이던 RestTemplate의 뒤를 잇는다. 4xx 응답은 HttpClientErrorException, 5xx 응답은 HttpServerErrorException, 연결 실패나 타임아웃은 ResourceAccessException으로 바뀌어 던져진다. 실제 통신은 요청 팩토리가 맡으며, 타임아웃도 요청 팩토리에 정한다.
톰캣의 스레드 모델
톰캣은 요청 처리용 스레드를 풀로 관리하며, 요청 하나를 스레드 하나가 맡아 응답이 끝날 때까지 처리한다. 동시에 처리할 수 있는 요청 수는 최대 스레드 수(기본 200)로 제한되고, 모든 스레드가 사용 중이면 새 요청은 대기한다. 요청 처리 중 데이터베이스나 외부 호출을 기다리는 동안에도 스레드는 점유된 상태다.
3-way handshake
TCP 연결을 맺을 때 거치는 세 단계의 과정이다. 클라이언트가 연결 요청(SYN)을 보내고, 서버가 수락과 함께 자신의 연결 요청(SYN+ACK)을 보내며, 클라이언트가 이를 확인(ACK)한다. 양쪽이 서로 보내고 받을 수 있는 상태임을 확인한 뒤 데이터 전송을 시작한다.
HTTP 상태 코드
서버가 요청을 처리한 결과를 나타내는 세 자리 숫자다. 2xx는 성공, 3xx는 다른 위치로의 이동(리다이렉션), 4xx는 클라이언트의 요청에 문제가 있음, 5xx는 서버 쪽 오류를 뜻한다. 200(성공), 201(생성됨), 400(잘못된 요청), 401(인증 필요), 403(권한 없음), 404(없음), 500(서버 오류)이 자주 쓰인다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. client.get() ... body(String.class) 호출: 주문 서버의 스레드 하나가 결제 서버를 호출하기 시작합니다. RestClient는 결과가 올 때까지 이 스레드를 붙잡아 두므로, 그동안 이 스레드는 다른 요청을 처리하지 못합니다.
  2. 연결 시도: HTTP 요청을 보내려면 먼저 결제 서버와 TCP 연결을 맺어야 합니다. 연결 요청을 보냅니다. 연결 타임아웃 2초를 이때부터 잽니다.
  3. 연결 성공: 결제 서버가 곧바로 연결을 받아 줍니다. 연결 타임아웃의 역할은 여기서 끝납니다. 이 뒤로 아무리 오래 걸려도 연결 타임아웃은 걸리지 않습니다.
  4. GET /payments/1 전송: 맺어진 연결로 요청을 보내고 응답을 기다립니다. 읽기 타임아웃은 정하지 않았습니다.
  5. 읽기 타임아웃 없음: 기다림을 끊어 줄 것이 없습니다. 연결 타임아웃(2초)은 연결이 맺어질 때 역할이 끝났으므로 여기서는 소용이 없습니다. 스레드는 응답이 올 때까지 10초 동안 묶여 있습니다.
  6. 응답 도착: 10초 뒤에 결제 서버가 200 응답을 보냅니다. RestClient가 본문을 문자열로 바꿔 돌려줍니다. 결과는 성공이지만, 스레드 하나가 10초 동안 다른 일을 하지 못했습니다.

실행 결과

코드

SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(2));   // 연결 타임아웃
// 읽기 타임아웃을 정하지 않음 (제한 없이 기다림)

RestClient client = RestClient.builder()
        .requestFactory(factory)
        .build();

try {
    String body = client.get()
            .uri("http://pay-server/payments/1")
            .retrieve()
            .body(String.class);            // 결과가 올 때까지 이 스레드는 여기서 기다린다
} catch (HttpClientErrorException e) {      // 4xx 응답
} catch (HttpServerErrorException e) {      // 5xx 응답
} catch (ResourceAccessException e) {       // 연결 실패, 타임아웃
}

이 모형의 전제

스물네 가지 조합을 모두 스프링 프레임워크 7.0.9의 RestClient(요청 팩토리는 SimpleClientHttpRequestFactory, JDK 21)로 실제 호출해, 결과와 예외의 종류, 예외의 원인, 기다린 시간, 상대 서버가 요청을 처리했는지가 화면과 일치하는 것을 확인했습니다. 상대 서버는 같은 PC의 127.0.0.1에 띄운 스프링 부트 4.1.1 서버(내장 톰캣)이고, 일부러 늦게 응답하는 주소를 두었습니다. 화면의 시간은 예시입니다. 실제 대조는 100분의 1로 줄여(연결 타임아웃 200ms, 읽기 타임아웃 300ms, 느린 응답 1000ms) 실행했고, 기다린 시간이 그 비율대로 나오는지 보았습니다. "연결 거절"은 아무 프로그램도 듣지 않는 127.0.0.1의 포트로 확인했습니다. "연결 요청에 답이 없음"은 127.0.0.1로는 만들 수 없어서, 문서용으로 예약되어 어느 장비에도 배정되지 않는 주소 192.0.2.1(RFC 5737)로 확인했습니다. 이 네 조합은 PC가 놓인 네트워크에 따라 결과가 다를 수 있습니다(네트워크가 끊겨 있으면 곧바로 실패합니다). 연결 타임아웃이 없을 때 운영체제가 포기하기까지 걸린 약 21초는 윈도우 11에서 본 값이며, 운영체제와 설정에 따라 다릅니다. 타임아웃을 정하지 않으면 제한 없이 기다린다는 것은 java.net.URLConnection 문서(타임아웃 0은 무한)에 따른 것이고, 실행으로는 응답이 올 때까지 기다리는 것까지만 확인했습니다. 읽기 타임아웃은 응답 전체에 걸리는 시간이 아니라 데이터가 오지 않는 채로 기다리는 시간의 한도라서, 조금씩 계속 오는 응답에는 걸리지 않을 수 있습니다(같은 문서 기준이며 실행으로 확인하지 않았습니다). 타임아웃을 정하는 방법과 기본값은 요청 팩토리의 종류에 따라 다릅니다. 대조는 호출 하나씩으로 했으므로, 요청이 몰려 스레드 풀이 바닥나는 상황은 실행으로 확인하지 않았습니다. 호출할 때마다 연결을 새로 맺는 경우로 단순화했고, 연결을 재사용하는 커넥션 풀과 그 대기 시간, 재시도, 서킷 브레이커는 다루지 않았습니다.

앞 실습: 테스트 · 다음 실습: 캐시 추상화

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요