메뉴 닫기

파이썬 requests 세션 재사용과 파일 핸들 누수 방지 가이드

파이썬 requests 세션 재사용과 파일 핸들 누수 방지 가이드

🐍 파일은 with 문으로 닫고, 세션은 재사용해 소켓 고갈을 막는 실전 체크포인트

프로덕션에서 HTTP 호출이 잦아지면 눈에 잘 띄지 않는 두 가지가 치명적인 병목이 됩니다. 파일 핸들을 열어두고 닫지 않은 채 방치하는 누수, 그리고 매 요청마다 새 requests.Session을 만들어 커넥션 풀을 끝없이 늘리는 습관입니다. 이 글은 실제 운영 환경에서 문제를 만들기 쉬운 패턴을 간단한 규칙으로 바꾸는 방법을 다룹니다. 코드 스타일을 크게 바꾸지 않고도 안정성과 처리량을 동시에 챙길 수 있는 방법을 정리해 드립니다. 작은 습관 교정만으로 경고 로그와 재시도, 타임아웃의 도미노를 예방할 수 있다는 점을 중심에 두었습니다.

핵심은 두 줄 요약으로 설명됩니다. 파일은 with open(…) 블록으로 열고, 네트워크는 가능한 한 동일한 requests.Session 인스턴스를 재사용합니다. 이 두 원칙을 지키면 파일 디스크립터 누수로 인한 Too many open files 오류와, 세션 난발로 인한 소켓 고갈 및 포트 에페메럴 고갈을 함께 줄일 수 있습니다. 또한 커넥션 재사용으로 지연 시간이 짧아지고, TLS 핸드셰이크 부담도 줄어 서비스 응답성이 좋아집니다. 이번 글에서는 기존 단일 호출 코드에서 자연스럽게 마이그레이션하는 순서와, 팀 규칙으로 굳히기 위한 점검표도 함께 제공합니다.



🔗 요청 코드 마이그레이션 원칙

파이썬 requests 모듈은 사용하기 간편하지만, 코드가 커질수록 무심코 작성한 한 줄이 성능 저하나 자원 누수의 원인이 되기 쉽습니다. 따라서 기존 요청 코드를 점검하고 개선하기 위한 마이그레이션 원칙을 세워두는 것이 중요합니다. 핵심은 ‘명시적 자원 관리’와 ‘세션 재활용’입니다.

기존 코드에서 흔히 볼 수 있는 패턴은 다음과 같습니다. 매번 requests.get()이나 requests.post()를 호출해 새 세션을 생성하고, 파일 업로드나 다운로드 시 open()으로 연 파일을 닫지 않은 채 반환하는 경우입니다. 이 두 가지는 짧은 코드에서는 문제없어 보이지만, 서버 호출이 수백 회 이상 누적되면 OS 수준에서 ‘파일 디스크립터 부족’ 오류가 발생할 수 있습니다.

CODE BLOCK
# 잘못된 예시
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()를 호출할 필요가 없습니다. 이 방식은 예외가 발생하더라도 안전하게 자원을 해제한다는 점에서 훨씬 안정적입니다.

CODE BLOCK
# 안전한 파일 처리 방식
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’와 같은 오류가 발생합니다. 즉, 세션을 무분별하게 생성하면 시스템이 자신을 공격하는 것처럼 포트를 소모하게 됩니다.

CODE BLOCK
# 비효율적인 코드 예시
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가 활성화되어 소켓 개수가 폭발적으로 증가하지 않습니다. 또한 인증 토큰, 공통 헤더, 재시도 정책 등을 세션 단위에서 설정할 수 있어 코드 관리도 훨씬 간결해집니다.

다음은 세션 재사용을 적용한 안전한 구조의 예시입니다. 핵심은 세션 객체를 모듈 레벨에서 전역으로 생성하고, 요청 함수들은 이를 받아서 사용하는 방식입니다.

CODE BLOCK
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 환경에서는 lsofnetstat, ss 명령어로 열려 있는 파일과 소켓을 실시간으로 확인할 수 있습니다.

CODE BLOCK
# 열린 파일 핸들 수 모니터링
lsof -p $(pgrep -f my_service) | wc -l

# 열려 있는 소켓 연결 확인
ss -s

또한 로그 분석 도구로 ResourceWarning: unclosed file이나 Connection pool is full 경고가 나타나는지 주기적으로 확인해야 합니다. 이는 이미 누수가 발생하고 있다는 명백한 신호입니다. 이러한 경고가 반복되면, 코드를 수정하기보다 먼저 세션 생성 로직을 추적하는 것이 우선입니다.

💎 핵심 포인트:
자원 관리는 “보이진 않지만 반드시 지켜야 하는 성능 계약”입니다. 세션과 파일 핸들을 철저히 관리하면, 서버의 안정성과 처리 효율이 몇 배로 향상됩니다.

자주 묻는 질문 (FAQ)

파일을 열었는데 닫지 않아도 자동으로 정리되지 않나요?
파이썬 인터프리터가 종료되면 대부분의 파일 핸들은 닫히지만, 장시간 실행되는 서비스에서는 자동 정리가 이루어지지 않습니다.
특히 예외가 발생한 경우 닫히지 않고 남아 메모리와 디스크립터를 점점 소모하게 됩니다.
반드시 with open() 구문으로 관리하세요.
requests.Session()을 매번 새로 만들어도 큰 차이가 없지 않나요?
개발 환경에서는 눈에 띄지 않지만, 실제 운영 환경에서는 수백 개의 TCP 커넥션이 쌓여 포트가 고갈될 수 있습니다.
세션을 재사용하면 커넥션을 재활용하기 때문에 훨씬 효율적이고 안정적입니다.
세션을 전역으로 만들면 메모리 누수가 생길 수 있나요?
세션 자체는 큰 메모리를 차지하지 않지만, 닫지 않고 프로그램을 반복 실행할 경우 캐시된 커넥션이 쌓일 수 있습니다.
프로그램 종료 시 session.close()를 명시적으로 호출하면 문제를 예방할 수 있습니다.
비동기 환경에서도 requests.Session()을 사용할 수 있나요?
비동기 환경에서는 requests 대신 aiohttp 같은 비동기 전용 클라이언트를 사용하는 것이 좋습니다.
requests.Session은 동기 코드용으로 설계되어 있으므로 async 환경에서는 오히려 효율이 떨어집니다.
파일 핸들 누수를 자동으로 감지할 수 있는 방법이 있나요?
네. lsof 명령어나 psutil 라이브러리를 활용해 프로세스가 열고 있는 파일 수를 주기적으로 확인할 수 있습니다.
테스트 환경에서는 pytest와 함께 pytest-leaks 플러그인을 사용하는 것도 좋은 방법입니다.
세션을 스레드마다 따로 만들어야 하나요?
네. requests.Session은 스레드 안전하지 않기 때문에, 멀티스레드 환경에서는 각 스레드별로 세션을 생성해야 합니다.
threading.local()을 사용하면 스레드마다 독립된 세션을 유지할 수 있습니다.
커넥션 풀의 크기를 조절할 수 있나요?
가능합니다. requests.adapters.HTTPAdapter를 사용해 풀 크기를 지정할 수 있습니다.
예를 들어 session.mount(‘https://’, HTTPAdapter(pool_connections=10, pool_maxsize=20)) 형태로 설정하면 됩니다.
with 문을 안 써도 try-finally로 닫으면 같은 효과인가요?
기능상 동일하지만, with 구문이 더 간결하고 예외 처리에도 안전합니다.
특히 여러 파일을 동시에 다룰 때는 중첩 with 구조가 훨씬 명확합니다.

📘 파이썬 requests 마이그레이션 핵심 정리

파이썬에서 requests 모듈은 손쉽게 HTTP 요청을 보낼 수 있는 도구지만, 운영 단계에서는 파일 핸들과 세션 관리의 사소한 실수가 서버 자원을 고갈시킬 수 있습니다.
이 글에서 다룬 핵심 원칙은 단순하지만, 서비스 안정성을 좌우하는 실전 가이드입니다.

첫째, 모든 파일 입출력은 with open() 구문으로 처리해 핸들을 자동으로 닫습니다.
둘째, 네트워크 요청은 반드시 requests.Session()을 재사용하는 구조로 설계해야 합니다.
이 두 가지를 지키면, 파일 디스크립터 누수나 소켓 고갈 문제를 근본적으로 차단할 수 있습니다.

또한 배포 전에는 세션이 과도하게 생성되지 않았는지, 파일이 제대로 닫히고 있는지 lsofss 명령어를 통해 점검하는 습관을 들이세요.
장기적으로 볼 때 이는 서버 다운을 막는 최고의 예방 조치입니다.
결국 코드를 간결하게 만드는 것보다 중요한 것은, 시스템이 오랜 시간 문제없이 돌아가는 것입니다.


🏷️ 관련 태그 : 파이썬requests, 파일핸들누수, 세션재사용, 소켓고갈, 커넥션풀, HTTP요청, 서버자원관리, 파이썬마이그레이션, 네트워크성능, 파이썬코딩팁