[트러블슈팅] 외부 API 호출 시 응답 지연(Timeout) 문제 구간별 추적법

지난 포스팅에서는 무한 루프나 잘못된 정규표현식으로 인해 서버의 CPU가 100%로 치솟는 현상을 스레드 덤프로 디버깅하는 방법을 알아보았습니다. 서버 내부의 문제를 완벽하게 통제하더라도, 현대의 애플리케이션은 결제 시스템, 본인 인증, 데이터 동기화 등 수많은 외부 API에 의존하여 동작합니다.

외부 시스템은 우리가 통제할 수 없는 미지의 영역입니다. 외부 서버가 느려지거나 네트워크 장비에 문제가 생기면, 우리 서버의 스레드들은 응답을 기다리며(Blocked) 서서히 멈춰가고 결국 전체 시스템의 ‘연쇄 장애’로 이어집니다. 이번 글에서는 외부 API 호출 시 발생하는 타임아웃(Timeout)의 종류를 명확히 구분하고, 어느 구간에서 병목이 발생했는지 추적하는 실전 노하우를 알아봅니다.

1. 타임아웃의 두 가지 얼굴: Connection Timeout vs Read Timeout

에러 로그에 TimeoutException이 찍혔다고 해서 다 같은 타임아웃이 아닙니다. 트러블슈팅의 첫걸음은 이 두 가지 타임아웃을 명확히 구분하는 것에서 시작됩니다.

  • Connection Timeout (연결 시간 초과): 클라이언트(우리 서버)가 외부 API 서버와 최초의 통신 연결(TCP 3-way Handshake) 자체를 맺지 못했을 때 발생합니다.
    • 주요 원인: 외부 서버가 완전히 다운되었거나, 중간에 있는 방화벽(Firewall)이 포트를 막고 있는 경우, 혹은 DNS 서버에 문제가 생겨 도메인의 IP를 찾지 못할 때 주로 발생합니다.
  • Read Timeout (읽기 시간 초과): 연결은 성공적으로 맺어졌고 요청(Request)도 정상적으로 보냈지만, 외부 서버가 정해진 시간 내에 응답(Response) 데이터를 주지 않을 때 발생합니다.
    • 주요 원인: 외부 서버가 살아있긴 하지만 내부의 무거운 DB 쿼리를 처리하느라 헉헉대고 있거나, 반환하는 데이터(JSON 등)의 크기가 네트워크 대역폭을 초과할 정도로 너무 거대할 때 발생합니다.

2. 구간별 지연 원인 추적(Troubleshooting) 기법

에러의 종류를 파악했다면, 이제 리눅스 터미널과 APM 도구를 활용해 정확히 어느 구간에서 패킷이 멈췄는지 추적해야 합니다.

  • 1단계: 네트워크 계층 확인 (curl -v): 가장 직관적인 방법입니다. 운영 서버의 터미널에서 curl -v "외부API주소"를 입력합니다. -v (verbose) 옵션을 주면 DNS 조회 시간, TCP 연결 시간, 실제 데이터 수신 시간을 상세히 쪼개서 보여줍니다. 여기서 Connection이 즉시 맺어지는지, 아니면 한참 대기하다가 끊어지는지 확인하여 네트워크 방화벽 문제인지 애플리케이션 문제인지 선을 긋습니다.
  • 2단계: 애플리케이션 스레드 상태 확인: APM(핀포인트, 데이터독 등)을 확인했을 때 특정 API의 응답 속도 그래프가 수직으로 치솟아 있다면 스레드 덤프를 추출합니다. 덤프 내역 중 대다수의 스레드가 java.net.SocketInputStream.socketRead0에 머물며 TIMED_WAITING 상태라면, 100% Read Timeout 병목 현상입니다. 즉, 우리 서버는 잘못이 없고 외부 서버가 느려서 우리 스레드들이 억울하게 대기하고 있는 상태입니다.

3. 연쇄 장애를 막는 방어 설계: 서킷 브레이커(Circuit Breaker)

외부 API의 장애가 우리 서버의 장애로 전파되는 것을 막으려면 ‘방어적 프로그래밍’이 필수적입니다.

  • 타임아웃은 ‘짧게’ 설정하라: Spring의 RestTemplate이나 WebClient를 사용할 때, 타임아웃을 아예 설정하지 않으면(무한 대기) 서버는 곧바로 죽습니다. Connection Timeout은 2~3초, Read Timeout은 5초 내외로 짧게 설정하여, 외부 서버가 느리다면 빠르게 포기하고 에러를 뱉게 만드는 것(Fail-Fast)이 훨씬 안전합니다.
  • 서킷 브레이커 (Resilience4j 등) 도입: 외부 API 서버가 심각한 장애 상태라고 판단되면, 아예 외부로 요청을 보내지 않고(차단기 작동) 우리 서버가 자체적으로 기본값(Fallback 데이터)을 반환하게 만드는 패턴입니다. 이를 통해 낭비되는 스레드 대기 시간을 0으로 만들고 시스템 전체를 보호할 수 있습니다.

결론: 외부 시스템을 절대 신뢰하지 마라

MSA 환경에서 엔지니어가 가져야 할 가장 중요한 마인드는 “외부 시스템은 언제든 지연되거나 죽을 수 있다”는 것입니다. 타임아웃은 에러가 아니라, 시스템을 보호하기 위한 안전장치입니다. 구간별 추적을 통해 병목의 원인을 외부로 명확히 소명하고, 서킷 브레이커와 적절한 타임아웃 설정으로 무장한 견고한 서버를 구축하시기 바랍니다.

네트워크 통신의 변수를 알아보았으니, 다음 포스팅에서는 인프라 환경의 변화가 가져온 새로운 유형의 장애, ‘[29번] [트러블슈팅] 컨테이너(Docker/Kubernetes) 환경에서의 JVM 메모리 제한 이슈’를 깊이 있게 파헤쳐 보겠습니다.

댓글 남기기