새벽 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())