파이썬 requests 세션 재사용과 파일 핸들 누수 방지 가이드
🐍 파일은 with 문으로 닫고, 세션은 재사용해 소켓 고갈을 막는 실전 체크포인트
프로덕션에서 HTTP 호출이 잦아지면 눈에 잘 띄지 않는 두 가지가 치명적인 병목이 됩니다. 파일 핸들을 열어두고 닫지 않은 채 방치하는 누수, 그리고 매 요청마다 새 requests.Session을 만들어 커넥션 풀을 끝없이 늘리는 습관입니다. 이 글은 실제 운영 환경에서 문제를 만들기 쉬운 패턴을 간단한 규칙으로 바꾸는 방법을 다룹니다. 코드 스타일을 크게 바꾸지 않고도 안정성과 처리량을 동시에 챙길 수 있는 방법을 정리해 드립니다. 작은 습관 교정만으로 경고 로그와 재시도, 타임아웃의 도미노를 예방할 수 있다는 점을 중심에 두었습니다.
핵심은 두 줄 요약으로 설명됩니다. 파일은 with open(…) 블록으로 열고, 네트워크는 가능한 한 동일한 requests.Session 인스턴스를 재사용합니다. 이 두 원칙을 지키면 파일 디스크립터 누수로 인한 Too many open files 오류와, 세션 난발로 인한 소켓 고갈 및 포트 에페메럴 고갈을 함께 줄일 수 있습니다. 또한 커넥션 재사용으로 지연 시간이 짧아지고, TLS 핸드셰이크 부담도 줄어 서비스 응답성이 좋아집니다. 이번 글에서는 기존 단일 호출 코드에서 자연스럽게 마이그레이션하는 순서와, 팀 규칙으로 굳히기 위한 점검표도 함께 제공합니다.
📋 목차
🔗 요청 코드 마이그레이션 원칙
파이썬 requests 모듈은 사용하기 간편하지만, 코드가 커질수록 무심코 작성한 한 줄이 성능 저하나 자원 누수의 원인이 되기 쉽습니다. 따라서 기존 요청 코드를 점검하고 개선하기 위한 마이그레이션 원칙을 세워두는 것이 중요합니다. 핵심은 ‘명시적 자원 관리’와 ‘세션 재활용’입니다.
기존 코드에서 흔히 볼 수 있는 패턴은 다음과 같습니다. 매번 requests.get()이나 requests.post()를 호출해 새 세션을 생성하고, 파일 업로드나 다운로드 시 open()으로 연 파일을 닫지 않은 채 반환하는 경우입니다. 이 두 가지는 짧은 코드에서는 문제없어 보이지만, 서버 호출이 수백 회 이상 누적되면 OS 수준에서 ‘파일 디스크립터 부족’ 오류가 발생할 수 있습니다.
# 잘못된 예시
import requests
def upload_file(path, url):
f = open(path, "rb") # 파일 닫지 않음
r = requests.post(url, files={"file": f}) # 매번 새 세션
return r.status_code
위 코드는 짧지만, 운영 서버에서는 위험합니다. 파일 핸들이 닫히지 않아 누적되고, 요청마다 새로운 소켓이 열리므로 OS 커널의 포트 풀이 빠르게 고갈됩니다. 따라서 다음 원칙에 따라 점진적으로 마이그레이션을 진행해야 합니다.
- 📘파일은 반드시 with open() 블록 안에서 열기
- 🔁HTTP 요청은 requests.Session()을 재사용하도록 구조 변경
- ⚙️세션 생성 및 종료는 애플리케이션 시작·종료 시점에 한정
- 🧩공유 세션 객체를 모듈 단위로 관리하여 재사용률 극대화
💎 핵심 포인트:
요청 구조를 리팩터링할 때 ‘기능별 분리’보다 ‘자원 관리’를 우선순위에 두세요. 짧은 코드라도 세션과 파일을 명시적으로 닫아야 시스템 안정성이 유지됩니다.
🗂️ 파일 핸들 누수 방지 with 문 패턴
파일 핸들 누수는 의외로 흔한 문제입니다. 특히 파일 업로드, 로그 저장, 데이터 임시 캐시 작업이 반복되는 환경에서 open()으로 연 파일을 닫지 않으면, 운영체제는 해당 핸들을 계속 유지합니다. 시간이 지나면 프로세스의 파일 디스크립터 한도(ulimit -n)를 초과하여 OSError: [Errno 24] Too many open files 오류가 발생할 수 있습니다.
이 문제는 파이썬의 with 문으로 매우 간단히 해결됩니다. with open() 구문을 사용하면 블록이 끝나는 즉시 파일이 자동으로 닫히므로, 명시적으로 f.close()를 호출할 필요가 없습니다. 이 방식은 예외가 발생하더라도 안전하게 자원을 해제한다는 점에서 훨씬 안정적입니다.
# 안전한 파일 처리 방식
import requests
def upload_file(path, url, session):
with open(path, "rb") as f:
response = session.post(url, files={"file": f})
return response.status_code
위 코드에서는 파일이 with 블록 안에서 열리고, 블록이 끝나는 즉시 자동으로 닫힙니다. 이렇게 하면 긴 시간 실행되는 백엔드 프로세스나 데이터 파이프라인에서도 자원 누수 없이 안정적으로 동작할 수 있습니다.
💡 TIP: with open()은 단순히 코드 스타일이 아니라, 예외 처리의 일환으로 생각해야 합니다. 특히 로깅, CSV, JSON 파일을 다루는 코드라면 반드시 with 패턴으로 통일하세요.
파일 누수를 방지하는 또 하나의 팁은 tempfile 모듈을 활용하는 것입니다. 임시 파일을 자동으로 생성하고 관리하며, with 문을 함께 사용하면 테스트나 데이터 변환 작업에서도 임시 파일이 자동으로 정리됩니다.
💬 운영 중인 코드에서 ‘파일을 닫지 않아도 된다’는 생각은 매우 위험합니다. 이는 일시적인 테스트 환경에서는 감지되지 않지만, 서비스 운영 중에는 점점 누적되어 결국 시스템을 정지시키는 주요 원인이 됩니다.
🌐 세션 무분별 생성의 위험과 소켓 고갈
많은 개발자들이 requests를 사용할 때 requests.get()이나 requests.post()를 매번 독립적으로 호출하는 방식을 선호합니다. 하지만 이는 내부적으로 매 요청마다 새로운 TCP 소켓을 열고 닫는 과정을 반복하므로, 소켓 고갈(socket exhaustion)을 초래할 수 있습니다. 특히 대량의 API 호출, 크롤러, 또는 비동기 큐 시스템에서는 이 문제가 치명적으로 나타납니다.
운영체제는 일반적으로 일정 개수의 ‘에페메럴 포트(ephemeral port)’를 할당받아 연결을 처리합니다. 세션을 매번 새로 열면 이 포트가 순식간에 소진되고, ‘[Errno 24] Too many open files’ 혹은 ‘ConnectionError: [Errno 99] Cannot assign requested address’와 같은 오류가 발생합니다. 즉, 세션을 무분별하게 생성하면 시스템이 자신을 공격하는 것처럼 포트를 소모하게 됩니다.
# 비효율적인 코드 예시
import requests
def fetch(url_list):
results = []
for url in url_list:
r = requests.get(url) # 매번 새로운 소켓 생성
results.append(r.text)
return results
위 코드는 요청 개수가 많을수록 시스템에 큰 부담을 줍니다. TCP 연결은 닫힌 후에도 일정 시간 동안 TIME_WAIT 상태를 유지하므로, 포트가 즉시 해제되지 않습니다. 결과적으로 몇천 개의 요청만 보내도 새 연결을 할당받지 못해 오류가 발생할 수 있습니다.
⚠️ 주의: 단순한 스크립트에서는 문제없어 보여도, 웹 서버나 워커 프로세스가 지속적으로 실행되는 환경에서는 세션 생성의 누적이 곧 리소스 누수가 됩니다. 커넥션 풀을 유지하지 않는 코드는 반드시 점검이 필요합니다.
이를 예방하려면 requests.Session() 객체를 한 번만 생성하고 여러 요청에서 재사용해야 합니다. 세션은 기본적으로 TCP 커넥션 풀을 관리하며, Keep-Alive를 통해 기존 소켓을 재활용하므로 성능과 안정성이 모두 개선됩니다.
💬 한 줄짜리 requests.get() 호출이 서버를 멈추게 할 수도 있습니다. 대량 요청을 처리하는 환경에서는 반드시 세션 재사용 구조로 마이그레이션해야 합니다.
🛡️ 안전한 세션 재사용 설계 패턴
세션을 재사용하는 가장 간단한 방법은 애플리케이션 단위에서 requests.Session()을 한 번만 생성해 공유하는 것입니다. 이렇게 하면 매 요청 시 TCP 커넥션 풀을 재활용하고, Keep-Alive가 활성화되어 소켓 개수가 폭발적으로 증가하지 않습니다. 또한 인증 토큰, 공통 헤더, 재시도 정책 등을 세션 단위에서 설정할 수 있어 코드 관리도 훨씬 간결해집니다.
다음은 세션 재사용을 적용한 안전한 구조의 예시입니다. 핵심은 세션 객체를 모듈 레벨에서 전역으로 생성하고, 요청 함수들은 이를 받아서 사용하는 방식입니다.
import requests
# 전역 세션 객체 생성
session = requests.Session()
session.headers.update({"User-Agent": "MyApp/1.0"})
def fetch_data(url):
r = session.get(url, timeout=10)
return r.json()
def send_file(path, url):
with open(path, "rb") as f:
r = session.post(url, files={"file": f})
return r.status_code
이 코드는 각 요청이 같은 세션을 사용하므로, 커넥션이 재활용되고 성능이 향상됩니다. 특히 keep-alive 상태를 유지한 덕분에 네트워크 지연이 줄어듭니다. 또한 HTTP 헤더를 한 번만 설정하면, 모든 요청에 자동으로 적용되므로 유지보수성도 좋아집니다.
💡 TIP: 멀티스레드 환경에서는 스레드마다 세션을 분리해야 합니다. 스레드 안전성을 위해 threading.local() 또는 requests.adapters.HTTPAdapter를 활용하면 커넥션 풀을 세밀하게 제어할 수 있습니다.
또한 세션 재사용 패턴은 테스트 코드에도 유리합니다. requests_mock이나 responses 라이브러리로 테스트할 때, 동일한 세션 객체를 주입받으면 실제 네트워크 호출을 막으면서 테스트 시나리오를 완전히 재현할 수 있습니다.
| 구분 | 세션 미사용 | 세션 재사용 |
|---|---|---|
| TCP 커넥션 | 매 요청마다 새로 생성 | 재활용 (Keep-Alive) |
| 성능 | 지연 증가, 오버헤드 큼 | 응답속도 향상 |
| 자원 사용량 | 소켓 고갈 위험 | 안정적 유지 |
요약하자면, requests.Session()을 사용하는 것은 선택이 아닌 필수입니다. 이는 단순히 속도 향상을 위한 트릭이 아니라, 장기적으로 서버 리소스를 보호하고 장애를 예방하는 구조적 원칙입니다.
🧭 배포 전 점검 체크리스트와 모니터링
파일 핸들 관리와 세션 재사용은 단순한 코드 스타일 문제가 아닙니다. 운영 환경에서는 이 두 요소가 직접적으로 서버 안정성과 성능에 영향을 미칩니다. 배포 전 반드시 아래 항목을 점검해두면, 장기적인 장애 예방과 자원 최적화에 큰 도움이 됩니다.
- 🧩모든 파일 입출력 코드가 with open() 블록 안에 있는가
- 🌐세션 객체가 모듈 레벨에서 한 번만 생성되고, 전역 재사용되는가
- ⚙️멀티스레드나 멀티프로세스 환경에서 세션 안전하게 분리되어 있는가
- 🧠요청 타임아웃(timeout) 값이 명시적으로 설정되어 있는가
- 📊운영 중 파일 디스크립터 수(lsof -p PID | wc -l)를 정기적으로 점검하는가
- 🔍세션 풀 크기와 재시도 정책을 모니터링 도구에서 추적하고 있는가
특히 장기 실행 프로세스(예: 크롤러, 데이터 수집기, 백엔드 API 서버)는 실행 중 파일 핸들과 소켓이 꾸준히 증가하는지 모니터링해야 합니다. Linux 환경에서는 lsof나 netstat, ss 명령어로 열려 있는 파일과 소켓을 실시간으로 확인할 수 있습니다.
# 열린 파일 핸들 수 모니터링
lsof -p $(pgrep -f my_service) | wc -l
# 열려 있는 소켓 연결 확인
ss -s
또한 로그 분석 도구로 ResourceWarning: unclosed file이나 Connection pool is full 경고가 나타나는지 주기적으로 확인해야 합니다. 이는 이미 누수가 발생하고 있다는 명백한 신호입니다. 이러한 경고가 반복되면, 코드를 수정하기보다 먼저 세션 생성 로직을 추적하는 것이 우선입니다.
💎 핵심 포인트:
자원 관리는 “보이진 않지만 반드시 지켜야 하는 성능 계약”입니다. 세션과 파일 핸들을 철저히 관리하면, 서버의 안정성과 처리 효율이 몇 배로 향상됩니다.
❓ 자주 묻는 질문 (FAQ)
파일을 열었는데 닫지 않아도 자동으로 정리되지 않나요?
특히 예외가 발생한 경우 닫히지 않고 남아 메모리와 디스크립터를 점점 소모하게 됩니다.
반드시 with open() 구문으로 관리하세요.
requests.Session()을 매번 새로 만들어도 큰 차이가 없지 않나요?
세션을 재사용하면 커넥션을 재활용하기 때문에 훨씬 효율적이고 안정적입니다.
세션을 전역으로 만들면 메모리 누수가 생길 수 있나요?
프로그램 종료 시 session.close()를 명시적으로 호출하면 문제를 예방할 수 있습니다.
비동기 환경에서도 requests.Session()을 사용할 수 있나요?
requests.Session은 동기 코드용으로 설계되어 있으므로 async 환경에서는 오히려 효율이 떨어집니다.
파일 핸들 누수를 자동으로 감지할 수 있는 방법이 있나요?
테스트 환경에서는 pytest와 함께 pytest-leaks 플러그인을 사용하는 것도 좋은 방법입니다.
세션을 스레드마다 따로 만들어야 하나요?
threading.local()을 사용하면 스레드마다 독립된 세션을 유지할 수 있습니다.
커넥션 풀의 크기를 조절할 수 있나요?
예를 들어 session.mount(‘https://’, HTTPAdapter(pool_connections=10, pool_maxsize=20)) 형태로 설정하면 됩니다.
with 문을 안 써도 try-finally로 닫으면 같은 효과인가요?
특히 여러 파일을 동시에 다룰 때는 중첩 with 구조가 훨씬 명확합니다.
📘 파이썬 requests 마이그레이션 핵심 정리
파이썬에서 requests 모듈은 손쉽게 HTTP 요청을 보낼 수 있는 도구지만, 운영 단계에서는 파일 핸들과 세션 관리의 사소한 실수가 서버 자원을 고갈시킬 수 있습니다.
이 글에서 다룬 핵심 원칙은 단순하지만, 서비스 안정성을 좌우하는 실전 가이드입니다.
첫째, 모든 파일 입출력은 with open() 구문으로 처리해 핸들을 자동으로 닫습니다.
둘째, 네트워크 요청은 반드시 requests.Session()을 재사용하는 구조로 설계해야 합니다.
이 두 가지를 지키면, 파일 디스크립터 누수나 소켓 고갈 문제를 근본적으로 차단할 수 있습니다.
또한 배포 전에는 세션이 과도하게 생성되지 않았는지, 파일이 제대로 닫히고 있는지 lsof와 ss 명령어를 통해 점검하는 습관을 들이세요.
장기적으로 볼 때 이는 서버 다운을 막는 최고의 예방 조치입니다.
결국 코드를 간결하게 만드는 것보다 중요한 것은, 시스템이 오랜 시간 문제없이 돌아가는 것입니다.
🏷️ 관련 태그 : 파이썬requests, 파일핸들누수, 세션재사용, 소켓고갈, 커넥션풀, HTTP요청, 서버자원관리, 파이썬마이그레이션, 네트워크성능, 파이썬코딩팁