<!doctype html>
2026년 6월 25일 | 개발 깨알 상식_Tips
API 클라이언트에서 timeout 하나만 두면 실패 이유가 자주 뭉개진다. 호출이 늦었다는 사실은 알겠는데, 실제로는 DNS나 TCP 연결에서 막힌 건지, 서버가 처리를 오래 한 건지, 본문을 읽는 중에 멈춘 건지 로그만 봐서는 갈라지지 않는다. 나도 작은 수집기를 만들 때 처음에는 timeout=30 하나로 끝냈다가, 같은 에러를 네트워크 문제와 서버 지연 문제로 번갈아 오해한 적이 있다.
timeout 하나가 숨기는 것
가장 흔한 실수는 timeout을 “전체 호출 제한 시간”처럼만 생각하는 것이다. 물론 어떤 라이브러리는 전체 예산을 지원하지만, 실무에서 내가 먼저 나누고 싶은 것은 connect timeout과 read timeout이다. connect timeout은 목적지까지 연결을 준비하는 구간이다. DNS 조회, TCP handshake, TLS 협상, 프록시 경유 같은 일이 여기 들어간다. read timeout은 요청이 나간 뒤 서버 응답을 기다리거나 본문을 읽는 구간이다.
두 구간은 원인이 다르다. connect 쪽이 자주 터지면 주소, 네트워크, 방화벽, 프록시, 컨테이너 DNS를 먼저 본다. read 쪽이 길면 서버 처리 시간, 쿼리 비용, 응답 크기, rate limit, 비동기 작업 상태를 먼저 본다. 둘을 같은 예외 메시지로 남기면 다음 사람이 볼 수 있는 정보가 “느렸다” 하나밖에 없다.
requests에서는 튜플부터 둔다
Python의 requests를 쓸 때는 timeout을 숫자 하나보다 튜플로 주는 편이 낫다. 첫 값은 연결 timeout, 둘째 값은 read timeout이다. 나는 외부 API 호출에서는 연결을 짧게, 읽기를 업무 성격에 맞게 조금 길게 잡는 쪽을 기본값으로 둔다. 연결이 10초씩 걸리는 상태라면 이미 주변 환경을 봐야 할 가능성이 높기 때문이다.
import requests
resp = requests.get(
"https://api.example.com/items",
timeout=(3.0, 20.0), # connect, read
)
resp.raise_for_status()
이 값이 정답이라는 뜻은 아니다. 내부망의 짧은 health check와 외부 리포트 생성 API는 예산이 다르다. 다만 숫자 하나로 적는 것보다 어느 구간에 얼마를 허용했는지가 코드에 남는다. 나중에 장애 로그를 볼 때도 “연결이 3초 안에 안 붙었다”와 “서버가 20초 안에 응답을 못 줬다”는 완전히 다른 문장이다.
timeout 숫자를 키우는 일도 마지막 선택에 가깝다. 호출이 가끔만 느려지는지, 특정 리전이나 특정 도메인에서만 느린지, 응답 크기가 커질 때만 느린지를 먼저 나눠 봐야 한다. 숫자를 30초에서 120초로 늘리면 장애가 사라진 것처럼 보일 수 있지만, 실제로는 worker가 오래 붙잡히고 queue가 밀리는 문제가 뒤로 숨어 버릴 때가 있다. 짧은 배치라면 실패를 빨리 드러내고 다음 항목으로 넘어가는 편이 전체 처리량에 더 낫기도 하다.
httpx는 예산 이름을 더 드러낸다
httpx를 쓰면 timeout 예산을 조금 더 이름 붙여 나눌 수 있다. connect, read, write, pool을 따로 둘 수 있어서 동시 호출이 많은 배치나 작은 API gateway에서 상태를 읽기 좋다. pool timeout은 특히 놓치기 쉽다. 실제 서버가 느린 게 아니라 커넥션 풀에서 차례를 기다리다 늦어지는 상황이기 때문이다.
import httpx
timeout = httpx.Timeout(
connect=3.0,
read=20.0,
write=10.0,
pool=5.0,
)
with httpx.Client(timeout=timeout) as client:
resp = client.get("https://api.example.com/items")
resp.raise_for_status()
이렇게 나눠 두면 로그 필드도 자연스럽게 나뉜다. 예외 타입, target host, attempt 번호, elapsed time, 어느 timeout 예산이 걸렸는지를 같이 남기면 된다. 자동 재시도 라이브러리를 붙이더라도 이 정보가 있어야 “더 기다릴지”, “대상을 줄일지”, “네트워크 설정을 볼지”를 덜 추측한다.
재시도는 별도 판단이다
timeout이 났다고 무조건 재시도하는 것도 위험하다. connect timeout은 순간적인 네트워크 흔들림이면 한두 번 재시도할 수 있지만, DNS 설정이 깨졌다면 반복 호출은 로그만 늘린다. read timeout은 서버가 실제로 요청을 처리했을 수도 있어서 더 조심해야 한다. POST나 결제, 작업 생성 요청처럼 side effect가 있는 호출이면 idempotency key나 작업 상태 조회 경로가 먼저 필요하다.
| 증상 | 먼저 볼 곳 | 다음 행동 |
|---|---|---|
| connect timeout | DNS, 프록시, 방화벽, host 오타 | 짧은 재시도 후 환경 점검 |
| read timeout | 서버 처리 시간, 응답 크기, 쿼리 비용 | 작업 분할 또는 상태 조회 |
| pool timeout | 동시성, 커넥션 풀 크기, 세션 재사용 | 호출량 제한과 풀 설정 조정 |
내가 남기는 최소 로그
나는 timeout을 잡을 때 예외 메시지 원문만 남기지 않는다. 최소한 method, host, timeout 종류, 시도 횟수, 걸린 시간, request id가 있으면 같이 남긴다. 429처럼 서버가 기다리라는 신호를 보낸 경우에는 Retry-After도 따로 남긴다. 이렇게 해 두면 timeout과 rate limit, 서버 오류가 한 줄짜리 “API 실패”로 섞이지 않는다.
운영 알림도 같은 이유로 너무 뭉뚱그리지 않는 편이 좋다. “외부 API timeout 증가”라는 알림은 처음 보는 사람에게 거의 도움이 안 된다. “connect timeout 3초 초과가 5분 동안 40건”인지, “read timeout 20초 초과가 특정 endpoint에서만 증가”인지 정도만 갈라져도 담당자가 바로 다른 대시보드를 연다. 작은 서비스일수록 이 구분이 과하다고 느끼기 쉬운데, 막상 한 번 장애를 겪으면 그 한 줄 차이가 크다.
결국 포인트는 멋진 네트워크 튜닝이 아니다. 느린 호출을 만났을 때 어디서 기다렸는지를 먼저 분리해 두자는 것이다. 연결이 늦은 호출과 응답이 늦은 호출은 고칠 사람이 다르고, 늘려야 할 숫자도 다르고, 재시도해도 되는 조건도 다르다. timeout을 두 값으로 나눠 적는 작은 차이가 다음 디버깅 시간을 꽤 줄여 준다.
'[개발 깨알 상식_Tips]' 카테고리의 다른 글
| 첫 실패 기준으로 줄인 pytest 로그 (0) | 2026.06.30 |
|---|---|
| 서버 동기화 직전의 삭제 파일 경고 (0) | 2026.06.26 |
| SQLite 백업에서 빠진 WAL 로그 (0) | 2026.06.24 |
| HTTP 429와 Retry-After 헤더 (0) | 2026.06.23 |
| 압축 파일 안의 절대 경로 덮어쓰기 위험 (0) | 2026.06.23 |