API를 호출했는데 프로그램이 한참 멈춰 있거나, 응답은 200인데 JSON 파싱에서 실패할 때가 있다. 모두 ‘요청 실패’로 뭉뚱그리면 어디부터 고쳐야 할지 알기 어렵다. 연결·HTTP 상태·응답 형식을 다른 단계로 보면 원인이 선명해진다.
requests 요청에서 timeout과 상태 코드는 왜 따로 볼까?
requests.get()은 응답 객체를 받는 단계다. 기본 설정만으로는 원하는 시간 안에 끝난다는 보장이 없다. 아래 함수는 연결과 응답 읽기에 각각 기다릴 시간을 둔 뒤, HTTP 오류를 확인하고 JSON을 읽는다.
import requests
def fetch_json(url: str):
response = requests.get(url, timeout=(3, 5))
response.raise_for_status()
return response.json()(3, 5)는 전체 작업을 8초 안에 끝내겠다는 뜻이 아니라 연결 3초·읽기 5초 설정이다. 읽기 제한도 전체 다운로드의 절대 마감 시간이 아니다. 이 차이는 느린 서버를 진단할 때 중요하다. Requests 공식 안내에서 timeout, 상태 검사, JSON 파싱의 서로 다른 실패를 확인할 수 있다.
200 응답이면 JSON도 정상일까?
아니다. 서버가 200 OK와 함께 HTML 오류 페이지를 보낼 수도 있다. raise_for_status()는 HTTP 4xx·5xx를 잡지만 본문의 형식까지 보장하지 않는다. JSON이 아닌 응답에 response.json()을 호출하면 파싱 오류가 난다.
| 관찰 | 실패한 단계 | 먼저 확인할 것 |
|---|---|---|
| 응답 객체를 받기 전 timeout | 연결 또는 읽기 | 대상 서버·네트워크·대기 시간 |
404, 500 등 HTTP 오류 | 상태 검사 | URL·요청 값·서버 상태 |
200인데 JSON 파싱 오류 | 응답 형식 | Content-Type·본문 앞부분 |
예를 들어 기대한 API가 JSON을 반환해야 하는데 Content-Type이 text/html이라면, 파싱 코드를 고치기 전에 잘못된 경로나 중간 로그인 화면을 받은 건 아닌지 확인하자. Content-Type만 맞다고 내용까지 유효한 것은 아니므로 마지막에는 실제 파싱 결과도 확인해야 한다.
실패를 어떻게 재현하고 디버깅할까?
실제 서비스 대신 테스트에서는 성공 JSON, 404 응답, 200 HTML 응답, 읽기 timeout을 각각 별개 사례로 만든다. 성공 사례만 테스트하면 raise_for_status() 누락과 JSON 파싱 실패를 놓치기 쉽다. 함수의 반환값을 모두 None으로 삼키지 말고, 어떤 단계에서 실패했는지 호출자가 알 수 있게 한다.
디버깅 로그에는 요청한 API의 일반 경로, 상태 코드, 예외 종류 정도만 남기자. 요청 헤더 전체, 인증 토큰, 응답 본문 전체는 민감정보를 담을 수 있다. 사용자가 임의의 URL을 넣을 수 있는 서버 코드라면 허용된 대상만 요청하도록 막아야 내부 주소를 대신 호출하는 위험도 줄일 수 있다.
재시도는 어떤 실패에만 할까?
일시적인 연결 실패에는 제한된 재시도가 도움이 될 수 있다. 하지만 잘못된 URL이나 인증 오류를 똑같이 반복하면 문제를 고치지 못하고 부하만 늘어난다. 읽기 timeout은 서버가 요청을 처리했으나 응답만 늦은 상황일 수도 있으므로, 특히 데이터를 바꾸는 요청을 재시도할 때는 중복 처리 가능성을 먼저 따져야 한다. 이 글의 GET 예제와 쓰기 요청의 재시도 정책을 그대로 같게 두지 말자.
핵심 요약
requests 호출은 timeout → HTTP 상태 → 응답 형식·JSON 파싱 순서로 확인한다. 200은 JSON의 유효성을 보장하지 않고, timeout도 전체 작업의 절대 마감 시간은 아니다. 실패 유형을 구분해 테스트하고, 로그·재시도에는 보안과 중복 처리 경계를 함께 둔다.

