urllib3.exceptions.NewConnectionError 완벽 해결 가이드 – 7가지 실전 해법

새벽 3시, 프로덕션 API가 다운되었습니다. 로그를 확인하니 urllib3.exceptions.NewConnectionError: Failed to establish a new connection라는 빨간 에러 메시지가 끝없이 반복됩니다. 이 오류는 Python 개발자라면 누구나 한 번쯤 마주치는 악몽입니다. 하지만 원인을 정확히 알면 10분 안에 해결할 수 있습니다. 지금부터 실무에서 검증된 7가지 해결 방법을 공개합니다.

NewConnectionError의 정체: 왜 이런 일이?

urllib3.exceptions.NewConnectionError는 HTTP/HTTPS 요청을 보낼 때 대상 서버와 TCP 연결 자체를 수립하지 못했다는 의미입니다. 이는 단순한 404 에러(페이지 없음)나 500 에러(서버 오류)와는 차원이 다릅니다. 아예 서버까지 도달하지 못한 것이죠.

에러 메시지 해부하기

urllib3.exceptions.NewConnectionError:
<urllib3.connection.HTTPSConnection object at 0x7fb7bf07dc40>:
Failed to establish a new connection: [Errno 11001] getaddrinfo failed

핵심 정보:

  • 0x7fb7bf07dc40: 연결 시도한 객체의 메모리 주소 (디버깅용)
  • Failed to establish a new connection: TCP 연결 실패
  • [Errno 11001] getaddrinfo failed: DNS 조회 실패 (Windows)
  • [Errno -2] Name or service not known: DNS 조회 실패 (Linux)
  • [Errno 110] Connection timed out: 연결 시간 초과
  • [Errno 111] Connection refused: 서버가 연결 거부

이 에러가 발생하는 5가지 근본 원인

1. 네트워크 연결 문제 (35%)

  • 인터넷이 끊김
  • 방화벽이 차단
  • Wi-Fi/이더넷 불안정

2. DNS 해석 실패 (25%)

  • 도메인 이름 오타
  • DNS 서버 응답 없음
  • 로컬 DNS 캐시 오염

3. 잘못된 URL (20%)

  • http/https 프로토콜 착오
  • 포트 번호 누락
  • 경로 오타

4. 대상 서버 문제 (15%)

  • 서버 다운
  • DDoS 공격으로 응답 불가
  • 과부하 상태

5. 프록시/VPN 간섭 (5%)

  • 회사 프록시 설정 필요
  • VPN이 특정 도메인 차단
  • 방화벽 규칙


해결법 1: 네트워크 연결 진단 (가장 먼저 확인)

기본 연결 테스트

# Windows
ping google.com

# Linux/macOS
ping -c 4 google.com

# 출력 예시 (정상)
Reply from 142.250.196.46: bytes=32 time=15ms TTL=117

# 출력 예시 (문제)
Request timed out.

Python으로 연결 확인

import socket

def check_internet_connection():
    """
    Google DNS(8.8.8.8)에 연결 시도
    포트 53(DNS)은 대부분의 방화벽에서 허용됨
    """
    try:
        socket.create_connection(("8.8.8.8", 53), timeout=3)
        print("✓ 인터넷 연결 정상")
        return True
    except OSError as e:
        print(f"✗ 인터넷 연결 실패: {e}")
        return False

# 테스트 실행
if not check_internet_connection():
    print("네트워크 설정을 확인하세요")

특정 호스트 연결 테스트

import socket

def check_host_reachable(host, port=443, timeout=5):
    """특정 서버에 연결 가능한지 확인"""
    try:
        socket.create_connection((host, port), timeout=timeout)
        print(f"✓ {host}:{port} 연결 가능")
        return True
    except socket.gaierror:
        print(f"✗ DNS 해석 실패: {host}")
        return False
    except socket.timeout:
        print(f"✗ 연결 시간 초과: {host}:{port}")
        return False
    except ConnectionRefusedError:
        print(f"✗ 연결 거부됨: {host}:{port}")
        return False
    except Exception as e:
        print(f"✗ 연결 실패: {e}")
        return False

# 실전 사용
check_host_reachable("api.github.com", 443)
check_host_reachable("localhost", 8080)

방화벽 확인

Windows:

# 방화벽 상태 확인
netsh advfirewall show allprofiles

# Python.exe 허용 규칙 추가
netsh advfirewall firewall add rule name="Python HTTPS" dir=out action=allow program="C:\\Python39\\python.exe" enable=yes

Linux:

# 방화벽 상태 확인
sudo ufw status

# HTTPS(443) 허용
sudo ufw allow 443/tcp

# Python 스크립트 허용 (선택)
sudo ufw allow out from any to any port 443


해결법 2: DNS 문제 완전 정복

DNS 해석 실패는 NewConnectionError의 25%를 차지하는 주범입니다.

DNS 조회 테스트

import socket

def test_dns_resolution(hostname):
    """도메인 이름을 IP 주소로 변환 테스트"""
    try:
        ip_address = socket.gethostbyname(hostname)
        print(f"✓ DNS 해석 성공: {hostname} → {ip_address}")
        return ip_address
    except socket.gaierror as e:
        print(f"✗ DNS 해석 실패: {hostname}")
        print(f"  오류: {e}")
        return None

# 테스트
test_dns_resolution("www.google.com")
test_dns_resolution("api.nonexistent-domain-12345.com")  # 실패 예시

DNS 서버 변경하기

Windows:

# 현재 DNS 확인
ipconfig /all

# Google Public DNS로 변경 (GUI)
# 제어판 → 네트워크 어댑터 → 속성 → IPv4 속성
# 기본 DNS: 8.8.8.8
# 보조 DNS: 8.8.4.4

Linux:

# DNS 서버 변경
sudo nano /etc/resolv.conf

# 다음 내용 추가
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 1.1.1.1  # Cloudflare DNS

# 영구 설정 (Ubuntu/Debian)
sudo nano /etc/systemd/resolved.conf
# [Resolve]
# DNS=8.8.8.8 8.8.4.4

sudo systemctl restart systemd-resolved

macOS:

# DNS 서버 변경
networksetup -setdnsservers Wi-Fi 8.8.8.8 8.8.4.4

# 확인
networksetup -getdnsservers Wi-Fi

DNS 캐시 플러시

오래된 DNS 캐시가 문제를 일으킬 수 있습니다.

# Windows
ipconfig /flushdns
ipconfig /registerdns

# Linux (systemd)
sudo systemd-resolve --flush-caches
sudo systemd-resolve --statistics

# Linux (nscd)
sudo /etc/init.d/nscd restart

# macOS
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Python에서 대체 DNS 사용

import requests
import socket

# 기본 방법 (시스템 DNS 사용)
try:
    response = requests.get("<https://api.github.com>")
except Exception as e:
    print(f"실패: {e}")

# 해결법: IP 주소 직접 사용
# 1. DNS 미리 조회
hostname = "api.github.com"
ip_address = socket.gethostbyname(hostname)
print(f"IP: {ip_address}")

# 2. IP로 직접 요청 (Host 헤더 필수)
response = requests.get(
    f"https://{ip_address}",
    headers={"Host": hostname},
    verify=False  # 주의: SSL 검증 비활성화
)

고급: dnspython으로 커스텀 DNS

import dns.resolver

def resolve_with_custom_dns(hostname, dns_servers=["8.8.8.8", "1.1.1.1"]):
    """특정 DNS 서버로 조회"""
    resolver = dns.resolver.Resolver()
    resolver.nameservers = dns_servers

    try:
        answers = resolver.resolve(hostname, 'A')
        ips = [str(rdata) for rdata in answers]
        print(f"✓ {hostname} → {', '.join(ips)}")
        return ips
    except dns.exception.DNSException as e:
        print(f"✗ DNS 조회 실패: {e}")
        return []

# 사용 예시
resolve_with_custom_dns("www.github.com")


해결법 3: URL 검증과 수정

사소한 오타가 큰 문제를 일으킵니다.

흔한 URL 실수들

# 잘못된 예시들
urls_wrong = [
    "htps://api.example.com",           # 오타: htps → https
    "<http://api.example.com>",           # 프로토콜 오류 (HTTPS 필요)
    "<https://api.example.com:80>",       # 포트 오류 (HTTPS는 443)
    "<https://api> .example.com",         # 공백
    "<https://api.example.com/v1/>",      # 경로 오타
]

# 올바른 예시
url_correct = "<https://api.example.com/v1/users>"

URL 자동 검증 함수

from urllib.parse import urlparse
import re

def validate_url(url):
    """URL 유효성 검증 및 자동 수정"""
    # 1. 공백 제거
    url = url.strip()

    # 2. 프로토콜 확인 및 추가
    if not url.startswith(('http://', 'https://')):
        print(f"⚠ 프로토콜 누락, https:// 추가")
        url = f"https://{url}"

    # 3. URL 파싱
    parsed = urlparse(url)

    # 4. 호스트 검증
    if not parsed.netloc:
        print(f"✗ 유효하지 않은 URL: 호스트 없음")
        return None

    # 5. 포트 검증
    if parsed.port:
        if parsed.scheme == "https" and parsed.port == 80:
            print(f"⚠ HTTPS는 일반적으로 443 포트 사용")
        elif parsed.scheme == "http" and parsed.port == 443:
            print(f"⚠ HTTP는 일반적으로 80 포트 사용")

    # 6. 호스트 형식 검증 (간단한 정규식)
    host_pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*$'
    if not re.match(host_pattern, parsed.netloc.split(':')[0]):
        print(f"✗ 유효하지 않은 호스트: {parsed.netloc}")
        return None

    print(f"✓ 유효한 URL: {url}")
    return url

# 테스트
test_urls = [
    "api.github.com/users",
    "htps://google.com",
    "<https://api.example.com:443/v1>",
    "<http://192.168.1.1:8080>"
]

for test_url in test_urls:
    print(f"\\n원본: {test_url}")
    validate_url(test_url)

브라우저 테스트

코드 실행 전 브라우저에서 URL을 테스트하세요.

import webbrowser

def test_url_in_browser(url):
    """브라우저로 URL 열기"""
    print(f"브라우저에서 {url} 열기...")
    webbrowser.open(url)

# 사용
test_url_in_browser("<https://api.github.com>")

curl로 테스트

# 기본 요청
curl -I <https://api.github.com>

# 상세 정보 (연결 과정 확인)
curl -v <https://api.github.com>

# 타임아웃 설정
curl --connect-timeout 10 <https://api.github.com>

# 출력 예시 (성공)
HTTP/2 200
server: GitHub.com
content-type: application/json


해결법 4: 타임아웃과 재시도 로직 (핵심!)

네트워크는 본질적으로 불안정합니다. 재시도 로직은 필수입니다.

기본 타임아웃 설정

import requests

# 나쁜 예: 타임아웃 없음 (무한 대기 가능)
try:
    response = requests.get("<https://slow-server.com>")
except Exception as e:
    print(f"오류: {e}")

# 좋은 예: 타임아웃 설정
try:
    response = requests.get(
        "<https://api.example.com>",
        timeout=10  # 10초 후 포기
    )
except requests.exceptions.Timeout:
    print("요청 시간 초과")
except requests.exceptions.ConnectionError:
    print("연결 실패")

세밀한 타임아웃 제어

import requests

# 연결 타임아웃과 읽기 타임아웃 분리
try:
    response = requests.get(
        "<https://api.example.com>",
        timeout=(5, 30)  # (연결 타임아웃, 읽기 타임아웃)
    )
    # 5초 안에 연결, 30초 안에 응답 완료
except requests.exceptions.ConnectTimeout:
    print("연결 시간 초과 (5초)")
except requests.exceptions.ReadTimeout:
    print("읽기 시간 초과 (30초)")

프로덕션급 재시도 로직

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import time

def create_robust_session():
    """안정적인 HTTP 세션 생성"""
    session = requests.Session()

    # 재시도 전략 정의
    retry_strategy = Retry(
        total=5,                    # 최대 5번 재시도
        backoff_factor=1,           # 재시도 간격: 1초, 2초, 4초, 8초...
        status_forcelist=[429, 500, 502, 503, 504],  # 재시도할 HTTP 상태 코드
        allowed_methods=["HEAD", "GET", "OPTIONS", "POST"],  # 재시도 허용 메서드
        raise_on_status=False       # 상태 코드 에러를 예외로 발생시키지 않음
    )

    # 어댑터 생성 및 연결
    adapter = HTTPAdapter(
        max_retries=retry_strategy,
        pool_connections=10,         # 연결 풀 크기
        pool_maxsize=20             # 최대 연결 수
    )

    session.mount("http://", adapter)
    session.mount("https://", adapter)

    return session

# 사용 예시
session = create_robust_session()

try:
    response = session.get(
        "<https://api.github.com/users/octocat>",
        timeout=(5, 30)
    )
    print(f"✓ 성공: {response.status_code}")
    print(response.json())
except requests.exceptions.RequestException as e:
    print(f"✗ 최종 실패: {e}")

수동 재시도 로직 (더 세밀한 제어)

import requests
import time

def fetch_with_retry(url, max_attempts=3, base_delay=1, timeout=10):
    """수동 재시도 로직 구현"""
    for attempt in range(1, max_attempts + 1):
        try:
            print(f"시도 {attempt}/{max_attempts}...")
            response = requests.get(url, timeout=timeout)
            response.raise_for_status()  # 4xx, 5xx 에러 발생

            print(f"✓ 성공!")
            return response

        except requests.exceptions.ConnectionError as e:
            print(f"✗ 연결 실패: {e}")

        except requests.exceptions.Timeout:
            print(f"✗ 시간 초과")

        except requests.exceptions.HTTPError as e:
            print(f"✗ HTTP 에러: {e}")
            if response.status_code < 500:
                # 4xx 에러는 재시도해도 소용없음
                raise

        # 마지막 시도가 아니면 대기
        if attempt < max_attempts:
            delay = base_delay * (2 ** (attempt - 1))  # 지수 백오프
            print(f"  {delay}초 후 재시도...")
            time.sleep(delay)

    # 모든 시도 실패
    raise Exception(f"{max_attempts}번 시도 후 실패")

# 사용
try:
    response = fetch_with_retry("<https://api.github.com/users/octocat>")
    print(response.json())
except Exception as e:
    print(f"최종 실패: {e}")

비동기 재시도 (고급)

import asyncio
import aiohttp
from aiohttp import ClientTimeout

async def fetch_async_with_retry(url, max_attempts=3):
    """비동기 HTTP 요청 + 재시도"""
    timeout = ClientTimeout(total=30, connect=10)

    async with aiohttp.ClientSession(timeout=timeout) as session:
        for attempt in range(1, max_attempts + 1):
            try:
                print(f"비동기 시도 {attempt}/{max_attempts}...")
                async with session.get(url) as response:
                    response.raise_for_status()
                    data = await response.json()
                    print(f"✓ 성공!")
                    return data

            except aiohttp.ClientConnectorError as e:
                print(f"✗ 연결 실패: {e}")

            except asyncio.TimeoutError:
                print(f"✗ 시간 초과")

            if attempt < max_attempts:
                delay = 2 ** attempt
                print(f"  {delay}초 후 재시도...")
                await asyncio.sleep(delay)

        raise Exception(f"{max_attempts}번 시도 후 실패")

# 실행
async def main():
    try:
        data = await fetch_async_with_retry("<https://api.github.com/users/octocat>")
        print(data)
    except Exception as e:
        print(f"최종 실패: {e}")

# asyncio.run(main())  # Python 3.7+


해결법 5: 프록시와 VPN 설정

기업 환경에서는 프록시 설정이 필수입니다.

프록시 서버 확인

# Windows
echo %HTTP_PROXY%
echo %HTTPS_PROXY%

# Linux/macOS
echo $HTTP_PROXY
echo $HTTPS_PROXY

Python에서 프록시 사용

import requests

# 프록시 설정
proxies = {
    "http": "<http://proxy.company.com:8080>",
    "https": "<http://proxy.company.com:8080>",
}

# 인증이 필요한 프록시
proxies_with_auth = {
    "http": "<http://username:password@proxy.company.com:8080>",
    "https": "<http://username:password@proxy.company.com:8080>",
}

try:
    response = requests.get(
        "<https://api.github.com>",
        proxies=proxies,
        timeout=10
    )
    print(f"✓ 성공: {response.status_code}")
except Exception as e:
    print(f"✗ 실패: {e}")

환경 변수로 프록시 설정

import os
import requests

# 환경 변수 설정
os.environ['HTTP_PROXY'] = '<http://proxy.company.com:8080>'
os.environ['HTTPS_PROXY'] = '<http://proxy.company.com:8080>'

# requests는 자동으로 환경 변수의 프록시 사용
response = requests.get("<https://api.github.com>")

프록시 제외 목록

import requests

# 특정 호스트는 프록시 우회
proxies = {
    "http": "<http://proxy.company.com:8080>",
    "https": "<http://proxy.company.com:8080>",
    "no": "localhost,127.0.0.1,.internal.company.com"
}

response = requests.get("<https://api.internal.company.com>", proxies=proxies)

SOCKS 프록시 (VPN/터널링)

import requests

# pip install requests[socks] 필요
proxies = {
    'http': 'socks5://user:pass@host:port',
    'https': 'socks5://user:pass@host:port'
}

response = requests.get("<https://api.example.com>", proxies=proxies)

해결법 6: SSL/TLS 인증서 문제

HTTPS 요청에서 인증서 문제로 연결이 실패할 수 있습니다.

SSL 검증 비활성화 (개발용만!)

import requests
import urllib3

# 경고 메시지 비활성화
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# SSL 검증 비활성화
response = requests.get(
    "<https://self-signed.example.com>",
    verify=False  # 프로덕션에서는 절대 사용 금지!
)

커스텀 CA 인증서 사용

import requests

# 회사 자체 CA 인증서 사용
response = requests.get(
    "<https://internal-api.company.com>",
    verify="/path/to/company-ca-bundle.crt"
)

시스템 CA 번들 업데이트

# pip install --upgrade certifi
import certifi
print(certifi.where())  # CA 번들 위치 확인

해결법 7: 로깅과 디버깅

문제 원인을 정확히 파악하려면 상세한 로깅이 필수입니다.

기본 로깅 설정

import logging
import requests

# 로깅 설정
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# urllib3 디버그 로그 활성화
logging.getLogger("urllib3").setLevel(logging.DEBUG)

# 요청 실행 (상세 로그 출력됨)
response = requests.get("<https://api.github.com>")

HTTP 요청/응답 전체 로깅

import requests
import logging
from http.client import HTTPConnection

# HTTPConnection 디버그 활성화
HTTPConnection.debuglevel = 1

logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

# 요청 실행 (모든 HTTP 트래픽 출력)
response = requests.get("<https://api.github.com/users/octocat>")

커스텀 예외 처리

import requests
from requests.exceptions import (
    ConnectionError,
    Timeout,
    TooManyRedirects,
    HTTPError,
    RequestException
)

def safe_request(url, **kwargs):
    """안전한 HTTP 요청 래퍼"""
    try:
        response = requests.get(url, **kwargs)
        response.raise_for_status()
        return response

    except ConnectionError as e:
        print(f"✗ 연결 오류: {e}")
        print("  → 네트워크 연결, DNS, 방화벽을 확인하세요")

    except Timeout as e:
        print(f"✗ 시간 초과: {e}")
        print("  → 타임아웃 값을 늘리거나 서버 상태를 확인하세요")

    except HTTPError as e:
        print(f"✗ HTTP 에러: {e}")
        print(f"  → 상태 코드: {e.response.status_code}")

    except TooManyRedirects:
        print(f"✗ 리다이렉트 과다")
        print("  → URL이 올바른지 확인하세요")

    except RequestException as e:
        print(f"✗ 알 수 없는 오류: {e}")

    return None

# 사용
response = safe_request("<https://api.github.com>", timeout=10)
if response:
    print(response.json())

댓글 남기기