메뉴 닫기

파이썬 requests 예외 처리 가이드 ConnectionError HTTPError Timeout TooManyRedirects SSLError 한 번에 정리

파이썬 requests 예외 처리 가이드 ConnectionError HTTPError Timeout TooManyRedirects SSLError 한 번에 정리

🧰 실무에서 바로 쓰는 예외 대응 체크리스트와 안전한 코드 패턴을 예제로 알려드립니다

프로젝트가 외부 API와 통신하기 시작하면, 성공 응답만 온다는 전제는 금방 깨집니다.
네트워크 단절, 인증서 문제, 끝없이 이어지는 리디렉션까지 다양한 상황이 코드 흐름을 멈추게 하죠.
특히 requests로 짠 간단한 스크립트가 운영 환경으로 올라가면 일시 장애나 간헐 오류가 곧바로 장애 전파로 이어질 수 있습니다.
그래서 예외를 정확히 구분하고, 재시도·타임아웃·로그를 체계화하는 습관이 중요합니다.
이번 글은 ConnectionError, HTTPError, Timeout, TooManyRedirects, SSLError처럼 자주 마주치는 핵심 예외를 중심으로 원인과 대응을 쉽고 실용적으로 정리합니다.

코드가 튼튼하려면 “모든 오류를 한꺼번에 except” 하는 방식에서 벗어나 구체적인 예외를 구체적으로 다루어야 합니다.
요청 단계별로 발생 지점을 파악하고, 타임아웃을 세분화하며, 리디렉션 한도를 명확히 지정하면 예측 가능한 실패를 만들 수 있습니다.
또한 인증서 검증을 임시로 끄는 위험을 이해하고 안전하게 우회하는 절차도 필요합니다.
글의 구성은 예외 구조 파악부터 각 예외의 실전 대응, 테스트와 진단 포인트까지 자연스럽게 흐르도록 배치했으니, 바로 실무 코드에 적용할 수 있는 형태로 가져가 보세요.



🧭 requests 예외 구조와 공통 패턴

파이썬 requests는 모든 네트워크 관련 오류의 뿌리로 requests.exceptions.RequestException을 제공합니다.
실무에서는 이 기본 예외를 맨 마지막 안전망으로 두고, 상황별 대표 하위 예외를 먼저 구체적으로 처리하는 방식이 가장 안전합니다.
핵심 하위 예외로는 ConnectionError (연결/네임해결/소켓 문제), HTTPError (응답이 성공이 아니고 raise_for_status() 호출 시 발생), Timeout (연결·읽기 지연), TooManyRedirects (리디렉션 루프/한도 초과), SSLError (TLS 인증서/핸드셰이크 문제)이 있습니다.
이 글 전반의 예제와 체크리스트는 이 다섯 가지를 안정적으로 다루는 데 초점을 맞춥니다.

예외 처리는 “무조건 재시도”보다는 실패를 분류하고, 타임아웃을 계층화하며, 리디렉션 한도TLS 검증 정책을 명확히 하는 방향이 중요합니다.
또한 로그에는 요청 식별자, 대상 호스트, 경과 시간, 상태 코드/예외명을 남겨야 이후 재현과 모니터링이 쉬워집니다.
아래 패턴은 최소한의 보일러플레이트로 이러한 원칙을 담은 기본 구조입니다.

CODE BLOCK
import requests
from requests.exceptions import ConnectionError, HTTPError, Timeout, TooManyRedirects, SSLError, RequestException

URL = "https://api.example.com/data"
TIMEOUT = (3.05, 5.0)  # (connect, read)
MAX_REDIRECTS = 5

try:
    with requests.Session() as s:
        s.max_redirects = MAX_REDIRECTS
        resp = s.get(URL, timeout=TIMEOUT)
        resp.raise_for_status()  # 4xx/5xx 시 HTTPError
        data = resp.json()
        # 처리 로직...
except Timeout as e:
    # 연결 또는 읽기 지연
    # 재시도(백오프) 또는 작업 큐로 위임
    print(f"Timeout: {e}")
except TooManyRedirects as e:
    # 잘못된 URL 또는 리디렉션 루프
    print(f"Too many redirects: {e}")
except SSLError as e:
    # 인증서/프로토콜 문제
    print(f"SSL error: {e}")
except ConnectionError as e:
    # DNS, 소켓, 연결 거부 등
    print(f"Connection error: {e}")
except HTTPError as e:
    # 상태코드 기반 분기(ex. 401 재인증, 429 백오프, 5xx 재시도)
    print(f"HTTP error: {e.response.status_code if hasattr(e, 'response') else 'unknown'}")
except RequestException as e:
    # 최종 안전망: 위에 해당하지 않는 모든 requests 예외
    print(f"Unhandled requests exception: {e}")

💬 예외는 구체적으로, 로그는 충분히.
재시도는 멱등 요청에 한정하고, 지수 백오프와 지터를 함께 적용하면 스파이크를 줄일 수 있습니다.

  • 🧱RequestException은 최후방에 두고, ConnectionError/HTTPError/Timeout/TooManyRedirects/SSLError를 먼저 분기
  • ⏱️timeout은 단일값 대신 튜플 (connect, read)로 세분화
  • 🔁세션의 max_redirects로 리디렉션 한도 명시, 의도치 않은 루프 차단
  • 🔐TLS 검증은 기본 유지, 필요 시 CA 번들을 명시하거나 사내 CA를 신뢰 저장소에 추가
  • 🧭HTTP 상태코드에 따라 동작 분기: 4xx는 입력/권한 점검, 429는 백오프, 5xx는 제한적 재시도

💡 TIP: 표준 Retry 로직이 필요하다면 urllib3.util.retry.RetryHTTPAdapter에 연결해 Session에 장착하면 코드 중복 없이 멱등 요청 재시도를 일괄 관리할 수 있습니다.

⚠️ 주의: verify=False로 TLS 검증을 끄면 SSLError는 사라질 수 있지만 중간자 공격 위험이 커집니다.
테스트 환경에서도 신뢰할 수 있는 CA 번들을 지정하는 방식으로 해결하는 것이 안전합니다.

🧪 ConnectionError 원인과 재시도 설계

서버에 연결조차 되지 않는 경우, ConnectionError가 발생합니다.
이는 DNS 해석 실패, 포트 닫힘, 네트워크 단절, 방화벽 차단 등 다양한 이유로 나타날 수 있습니다.
특히 외부 API 호출이 많은 서비스라면 이 예외는 일시적인 네트워크 끊김을 의미할 때가 많기 때문에,
바로 실패로 처리하기보다는 백오프 기반 재시도 로직을 설계하는 것이 좋습니다.

단, 모든 요청을 무조건 재시도하는 것은 위험합니다.
POST처럼 멱등성이 보장되지 않는 요청은 동일 데이터가 중복 전송될 수 있습니다.
따라서 재시도는 GET, HEAD, OPTIONS처럼 안전한 메서드에만 적용하고,
실패 시 로그를 남긴 뒤 즉시 중단하는 설계를 병행해야 합니다.

CODE BLOCK
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry_strategy = Retry(
    total=3,
    backoff_factor=0.5,   # 재시도 간격: 0.5s, 1s, 2s...
    status_forcelist=[500, 502, 503, 504],
    allowed_methods=["GET", "HEAD"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)

try:
    res = session.get("https://example.com/api", timeout=(3.05, 5))
    res.raise_for_status()
except requests.exceptions.ConnectionError as e:
    print("Connection failed:", e)

이 구조는 연결 오류뿐 아니라 일시적인 서버 오류(5xx)에도 자동 백오프를 적용합니다.
재시도 횟수와 대기 간격은 트래픽 성격에 따라 조정할 수 있으며,
백엔드 로드 밸런서나 게이트웨이 앞단에서 큐잉이 발생하는 환경에서는 지수 백오프 + 랜덤 지터를 추가해 충돌을 피하는 것이 권장됩니다.

💎 핵심 포인트:
ConnectionError는 단순 실패가 아니라 네트워크 환경 불안정의 신호입니다.
빈번하다면 DNS 캐시, VPN, 프록시 설정을 검토하고 서버 단의 Keep-Alive 정책도 확인하세요.

원인 대응 방법
DNS 이름 해석 실패 도메인 오타 또는 네임서버 점검
연결 거부 (Connection Refused) 서버 포트/방화벽 설정 확인
소켓 타임아웃 timeout 파라미터 조정 및 재시도 적용

⚠️ 주의: 재시도는 시스템 안정성을 높일 수 있지만, 잘못 사용하면 서버 부하를 가중시킬 수 있습니다.
전사적인 API 호출 정책 문서와 연동하여 허용 범위를 명확히 정의하세요.



📡 HTTPError 상태코드와 예외 처리

HTTPError는 서버가 요청을 받아들였지만, 응답 코드가 4xx 또는 5xx로 실패를 의미할 때 발생합니다.
이 예외는 response.raise_for_status() 호출 시 자동으로 트리거됩니다.
따라서 성공/실패 여부를 수동으로 검사하기보다는 이 메서드를 적극적으로 활용하면 코드의 가독성이 높아지고 예외 흐름을 일관성 있게 유지할 수 있습니다.

특히 API 서버가 명확한 상태코드 체계를 제공한다면, 각 코드별 대응 방식을 사전에 정의해 두는 것이 좋습니다.
예를 들어 401(인증 실패)은 토큰 갱신 루틴으로, 403(권한 없음)은 접근 제한 경고로, 429(요청 한도 초과)는 일정 시간 대기 후 재시도로 전환할 수 있습니다.
이처럼 HTTPError는 단순 예외가 아니라 비즈니스 로직의 분기 신호로 활용할 수 있습니다.

CODE BLOCK
import requests
from requests.exceptions import HTTPError

url = "https://api.example.com/user"
headers = {"Authorization": "Bearer invalid_token"}

try:
    resp = requests.get(url, headers=headers)
    resp.raise_for_status()
except HTTPError as e:
    status = e.response.status_code
    if status == 401:
        print("인증이 만료되었습니다. 토큰을 갱신하세요.")
    elif status == 403:
        print("접근 권한이 없습니다.")
    elif status == 404:
        print("요청한 리소스를 찾을 수 없습니다.")
    elif status == 429:
        print("요청이 너무 많습니다. 잠시 후 다시 시도하세요.")
    elif 500 <= status < 600:
        print("서버 오류입니다. 재시도 로직을 고려하세요.")
    else:
        print(f"예상치 못한 HTTP 오류: {status}")

예외 객체의 response 속성을 통해 상태코드와 본문, 헤더를 모두 확인할 수 있습니다.
로그에는 반드시 이 정보를 포함해두면 문제 재현이 쉬워집니다.
또한 API 제공자가 에러 메시지를 JSON으로 반환하는 경우,
response.json()을 통해 세부 오류 원인을 파싱해 사용자에게 구체적인 피드백을 제공할 수 있습니다.

상태 코드 의미 및 권장 대응
400 요청 형식 오류. 파라미터 및 JSON 유효성 검사 필요
401 인증 실패. 토큰 만료 또는 자격 증명 확인
403 권한 부족. 사용자 역할 또는 API 권한 설정 점검
404 리소스 없음. 경로 또는 ID 값 재검증
429 요청 한도 초과. Retry-After 헤더 값만큼 대기 후 재시도

💬 HTTPError를 단순히 예외로 넘기지 말고, 상태 코드별로 의미를 정의하면 운영 중 장애 원인을 훨씬 빠르게 파악할 수 있습니다.

💎 핵심 포인트:
HTTPError는 실패 신호가 아닌 통제된 예외 흐름으로 다루세요.
로그 수집과 모니터링을 연동하면 API 안정성을 지속적으로 높일 수 있습니다.

⏱️ Timeout 전략과 세분화

네트워크 요청의 timeout은 장애의 전파를 막아주는 1차 안전장치입니다.
기본값이 무제한(None)에 가깝다는 점을 간과하면, 외부 API 지연이 애플리케이션 스레드를 오래 붙잡아 두고 전체 시스템의 처리량을 갉아먹을 수 있습니다.
따라서 모든 요청에 명시적으로 타임아웃을 지정하고, 연결과 읽기 단계로 나누어 세분화하는 것이 중요합니다.
파이썬 requeststimeout=(connect, read) 형태의 튜플을 받아 두 구간을 개별적으로 제어할 수 있으며, 서비스 특성에 맞춘 합리적 기본값을 팀 규약으로 고정하면 운영 품질이 크게 좋아집니다.

연결 타임아웃은 네임해결과 TCP 핸드셰이크가 지연될 때 빠르게 포기하기 위한 값으로 수 초 이내가 적당합니다.
읽기 타임아웃은 서버가 응답 본문을 전송하기 시작한 후 청크를 기다리는 최대 시간을 의미하므로 API의 평균/최악 응답 시간을 고려해 더 넉넉히 잡습니다.
대역폭이 낮거나 큰 파일을 스트리밍할 때는 지나치게 공격적인 읽기 타임아웃이 오히려 다운로드 실패를 유발할 수 있으니 단계별로 점검이 필요합니다.

CODE BLOCK
import requests
from requests.exceptions import Timeout

def fetch_json(url: str, connect_timeout=3.05, read_timeout=8.0):
    try:
        r = requests.get(url, timeout=(connect_timeout, read_timeout))
        r.raise_for_status()
        return r.json()
    except Timeout as e:
        # 관측 가능한 타임아웃 로그에 구간 정보 포함
        raise Timeout(f"Timeout(connect={connect_timeout}, read={read_timeout}) for {url}") from e

# 사용 예: API는 빠르지만 네트워크 품질이 불안정한 환경
data = fetch_json("https://api.example.com/v1/items", connect_timeout=2.0, read_timeout=6.0)

여러 호출을 병렬로 처리한다면, 호출자 레벨에서 전체 작업 한도를 별도로 두는 것이 좋습니다.
요청별 connect/read 타임아웃과는 별도로, 스레드풀/프로세스풀/비동기 태스크의 총 소요 시간 상한을 설정해 전체 작업이 늪에 빠지지 않도록 합니다.
동시에, Retry 로직과 타임아웃의 곱이 과도한 지연을 만들지 않도록 최대 재시도 횟수와 백오프 상한을 정의하세요.

시나리오 connect, read 권장값
내부망, 지연이 매우 낮은 API (1.0, 3.0) — 실패를 빠르게 감지
공용 인터넷을 통한 일반 REST API (2.0, 6.0) — 평균 응답 1~2초 가정
대용량 다운로드/업로드(스트리밍) (3.0, 30.0+) — 청크 지연을 고려

💬 타임아웃 미설정은 장애가 아닌 운에 맡기는 것입니다.
모든 외부 호출에 정책적 기본값을 의무화하고, 서비스별 오버라이드를 허용하세요.

💡 TIP: 총 소요 시간을 강제하려면 작업을 ThreadPoolExecutor로 감싸서 future.result(timeout=…)를 사용하거나, 비동기 환경에서는 asyncio.wait_for로 상위 레벨 타임아웃을 추가하세요.

⚠️ 주의: timeout을 너무 짧게 잡으면 정상 트래픽도 잦은 재시도로 전환되어 서버와 클라이언트 모두에 부담이 됩니다.
실측 지표(RTT, p95 응답시간)를 기반으로 조정하고, 배포 전 부하 테스트로 검증하세요.



🔁 TooManyRedirects 및 리디렉션 한도

웹 요청 중 “끝없이 돌아가는 URL” 문제를 겪은 적이 있다면,
그 배후에는 TooManyRedirects 예외가 숨어 있을 가능성이 높습니다.
이 예외는 요청이 30x 응답을 반복하며 한도를 초과할 때 발생합니다.
기본적으로 requests는 내부적으로 최대 30회의 리디렉션을 허용하지만,
API나 잘못된 URL 패턴에서 순환 참조가 생기면 프로그램이 무한 루프처럼 지연될 수 있습니다.
이때는 Session.max_redirects 속성이나 allow_redirects=False 옵션으로 제어할 수 있습니다.

리디렉션은 편리하지만, 인증 토큰이 URL 파라미터로 전달되거나 HTTPS → HTTP로 다운그레이드되는 경우 보안 리스크가 생깁니다.
특히 OAuth 인증 과정이나 SSO 요청처럼 쿠키와 리디렉션이 함께 얽히는 환경에서는 반드시 도메인 검증Redirect-URL 화이트리스트 정책을 마련해야 합니다.

CODE BLOCK
import requests
from requests.exceptions import TooManyRedirects

session = requests.Session()
session.max_redirects = 5  # 기본 30 → 5회로 제한

try:
    response = session.get("https://example.com/redirect", timeout=5)
    print(response.url)
except TooManyRedirects as e:
    print("리디렉션 한도를 초과했습니다:", e)
except requests.exceptions.RequestException as e:
    print("기타 요청 오류:", e)

위 예제처럼 한도를 낮추면 의도치 않은 순환 리디렉션에서 빠르게 벗어날 수 있습니다.
또한 allow_redirects=False 옵션으로 리디렉션을 직접 제어하고,
헤더의 Location 값을 파싱해 새로운 요청을 수행하는 방법도 있습니다.
이 경우, URL을 검증해 신뢰할 수 있는 도메인으로만 요청을 이어가도록 제한하면 보안성을 강화할 수 있습니다.

리디렉션 유형 특징 및 주의점
301, 302 영구/임시 이동. 메서드 유지 여부는 서버 구현에 따라 다름
303 POST 후 GET으로 전환. Form 처리 후 리디렉션에 주로 사용
307, 308 요청 메서드 유지 보장. PUT/POST 시 주의 필요

💎 핵심 포인트:
리디렉션이 반복된다면 API 설계상 URL 순환 구조를 점검해야 합니다.
또한 크롤러나 봇 스크립트라면 리디렉션 히스토리를 기록해 분석에 활용하세요.

⚠️ 주의: allow_redirects=True 상태에서 HTTP → HTTPS 다운그레이드는 자동으로 차단되지 않습니다.
보안이 중요한 요청에서는 URL 검증 로직을 반드시 추가하세요.

자주 묻는 질문 FAQ

requests에서 모든 예외를 한 번에 잡으려면 어떻게 해야 하나요?
모든 예외의 상위 클래스인 requests.exceptions.RequestException을 사용하면 됩니다.
다만, 이렇게 하면 세부 원인을 구분할 수 없으므로 최후의 방어선으로만 사용하는 것이 좋습니다.
ConnectionError와 Timeout은 어떻게 구분되나요?
ConnectionError는 연결 자체가 실패한 경우(DNS, 소켓, 거부)이고,
Timeout은 연결 또는 읽기 시간이 지정된 한도를 초과한 경우입니다.
로그에 예외 이름과 메시지를 함께 남기면 구분이 쉽습니다.
HTTPError를 자동으로 raise하려면 어떻게 하나요?
response.raise_for_status()를 호출하면 4xx/5xx 응답 시 HTTPError가 발생합니다.
이 방법은 응답 검증 로직을 간결하게 만들어줍니다.
TooManyRedirects를 막으려면 설정을 어디서 변경하나요?
requests.Session 객체의 max_redirects 속성을 변경하거나,
get() 메서드 호출 시 allow_redirects=False로 지정하면 됩니다.
SSLError가 발생할 때 verify=False를 써도 될까요?
가능은 하지만 매우 위험합니다.
테스트용이 아니라면 인증서 검증을 반드시 유지하고,
자체 인증서를 사용하는 환경에서는 verify=”경로/ca-bundle.pem” 형태로 신뢰할 수 있는 번들을 명시하세요.
Retry 로직은 requests에서 기본 지원되나요?
requests 자체에는 자동 재시도 기능이 없습니다.
대신 urllib3.util.retry.Retry를 이용해 HTTPAdapter에 연결하면 안전하게 구현할 수 있습니다.
timeout을 전역 설정으로 지정할 수 있나요?
requests에는 전역 timeout 개념이 없습니다.
그러나 Session 래퍼 함수를 만들어 모든 호출에 공통 timeout 값을 넣으면 비슷한 효과를 낼 수 있습니다.
HTTP 상태코드별 사용자 정의 예외를 만들 수 있나요?
가능합니다.
HTTPError를 상속받은 커스텀 예외 클래스를 정의하고 상태코드에 따라 raise하면,
서비스별 정책 예외 흐름을 통일할 수 있습니다.

🧩 requests 예외 처리를 제대로 이해해야 하는 이유

파이썬 requests 모듈은 간단해 보이지만, 예외 구조를 이해하지 못하면 서비스 안정성에 큰 영향을 줍니다.
연결, 타임아웃, 리디렉션, 인증서 오류는 모두 다르게 대응해야 하는 성격의 문제입니다.
이 글에서 살펴본 ConnectionError, HTTPError, Timeout, TooManyRedirects, SSLError를 구분하고,
각 상황에 맞는 로그·재시도·알림·정책을 설계하면 시스템의 복원력이 한 단계 올라갑니다.

또한 단순히 예외를 처리하는 수준을 넘어, 예외를 설계에 포함시키는 것이 중요합니다.
예외를 ‘비정상 흐름’으로만 보지 말고, 통제된 응답으로 설계하면 장애를 예측하고 회피할 수 있습니다.
이런 접근이 장기적으로 API 품질을 개선하고, 관측 가능한 시스템(Observable System)을 만드는 출발점이 됩니다.

💎 핵심 포인트:
예외 처리는 단순히 에러를 잡는 기술이 아니라, 안정적 서비스 운영의 언어입니다.
requests를 사용할 때는 항상 구체적 예외, 합리적 타임아웃, 명시적 리디렉션 정책을 기억하세요.


🏷️ 관련 태그 : 파이썬requests, 예외처리, ConnectionError, HTTPError, Timeout, TooManyRedirects, SSLError, 네트워크프로그래밍, 파이썬개발, API통신