파이썬(Python) 셀레니움(Selenium) 좀비 프로세스 메모리 누수 완벽 해결 아키텍처

"리눅스 서버에서 크롤링 스크립트를 크론탭(Crontab)으로 돌려놨는데, 3일 만에 메모리가 100% 꽉 차서 서버 전체가 다운되었습니다."

파이썬(Python)과 셀레니움(Selenium)을 활용해 웹 자동화 로직을 짜본 B2B 데이터 엔지니어라면 100% 겪게 되는 통과의례다. 코드는 분명 완벽하게 작성된 것 같은데 시간이 지날수록 서버가 느려지다 결국 SSH 접속조차 불가능해진다. 터미널에서 `htop` 명령어를 쳐보면, 백그라운드에 수십 개의 '크롬(Chrome)'과 '크롬 드라이버(ChromeDriver)' 프로세스가 기괴하게 살아 숨 쉬며 램(RAM)을 파먹고 있는 좀비 프로세스(Zombie Process) 현상을 목격하게 된다.

이 치명적인 메모리 누수를 해결하지 못하면 서버를 매일 재부팅해야 하는 원시적인 노동에 시달려야 한다. 본 가이드에서는 프로세스 고아화(Orphaning)의 근본 원인을 해체하고, 스크립트 종료 시 관련 자원을 OS 레벨에서 완벽하게 말살하는 방어 아키텍처를 단호하게 제시한다.

1단계: 핵심 원인 분석 (예외 처리의 부재와 프로세스 고아화)

에러의 원인은 파이썬 코드 내의 '비정상 종료'에 있다. 크롤링 도중 웹페이지 로딩 지연이나 요소를 찾지 못해 에러(Timeout Exception 등)가 발생하면, 파이썬 스크립트는 빨간색 에러를 뿜으며 그 자리에서 즉사해 버린다. 문제는 파이썬이 죽기 전에 띄워놓았던 크롬 브라우저 창은 닫히지 않은 채 시스템 백그라운드에 고스란히 버려진다는 것이다. 주인이 죽어버린 브라우저(고아 프로세스)는 누구의 통제도 받지 않고 영원히 메모리를 점유한다.

2단계: 1차 방어선 - Context Manager와 Try-Finally 구조화

가장 기본적이자 필수적인 조치는 에러가 나든 안 나든, 스크립트가 종료되기 직전에 무조건 브라우저를 닫는 명령어(`driver.quit()`)를 실행하도록 코드를 강제하는 것이다. 이를 위해 `try...finally` 블록을 구성해야 한다.

(주의) 초보자들은 `driver.close()`를 사용하지만, 이는 현재 탭만 닫을 뿐 드라이버 백그라운드 프로세스는 남겨둔다. 반드시 프로세스 전체를 종료하는 `driver.quit()`을 사용해야 한다.

3단계: 완벽한 해결책 - OS 레벨의 좀비 프로세스 킬러(Killer) 도입

하지만 드라이버 자체가 프리징(Freezing)되어 `driver.quit()` 마저 응답하지 않는 악랄한 상황도 빈번하다. 진정한 아키텍처는 운영체제(OS)의 권한을 빌려 스크립트 시작 전후에 잔존하는 모든 브라우저 찌꺼기를 색출해 소각하는 함수를 배치하는 것이다.

import os
import signal
import psutil
from selenium import webdriver

def kill_zombie_processes():
    print("🧟 좀비 프로세스 사냥 가동: 잔여 크롬 및 드라이버 소각 중...")
    target_processes = ['chrome', 'chromedriver', 'chromium']
    killed_count = 0
    
    for proc in psutil.process_iter(['pid', 'name']):
        try:
            # 대소문자 구분 없이 타겟 프로세스 색출
            if any(target in proc.info['name'].lower() for target in target_processes):
                os.kill(proc.info['pid'], signal.SIGTERM)
                killed_count += 1
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
            
    print(f"정리 완료. 총 {killed_count}개의 좀비 프로세스 사살됨.")

def run_secure_crawler():
    # 스크립트 시작 전 이전 실행의 찌꺼기 청소
    kill_zombie_processes()
    
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage') # 메모리 초과 방지 필수 옵션
    
    driver = webdriver.Chrome(options=options)
    
    try:
        driver.get('https://example.com')
        # 데이터 수집 로직 실행
        print("데이터 수집 완료")
    except Exception as e:
        print(f"크롤링 중 치명적 에러 발생: {e}")
    finally:
        # 에러 유무와 상관없이 반드시 리소스 반환
        driver.quit()
        print("브라우저 자원 반환 완료")

if __name__ == "__main__":
    run_secure_crawler()

아키텍트의 시선 (Insight)

자동화 스크립트를 서버에 배포하는 순간, 당신의 코드는 실험실의 장난감이 아니라 거친 야생에 던져진 생명체가 된다. 자원을 빌려 썼다면 반드시 반환(Garbage Collection)하는 것은 프로그래밍의 가장 기본적인 철학이다. 좀비 프로세스 사냥꾼 로직과 `try-finally` 아키텍처를 당신의 모든 파이프라인에 이식하라. 무중단 시스템(Zero-downtime)은 완벽한 코드에서 나오는 것이 아니라, 완벽한 사후 처리에서 완성된다.

댓글

이 블로그의 인기 게시물

Zapier & Make.com 자동화의 덫: 무한 루프(Infinite Loop) 에러 완벽 방어 아키텍처

엑셀 보고서의 종말: 구글 스프레드시트와 루커 스튜디오(Looker Studio)로 실시간 대시보드 구축하기

Docker OOMKilled (Exit Code 137) 에러의 진실: 컨테이너 메모 누수 방어 및 리소스 최적화 아키텍처