메뉴 닫기

파이썬 성능 최적화: cProfile, pstats로 핫스폿을 찾아 가속하는 완벽 가이드

파이썬 성능 최적화: cProfile, pstats로 핫스폿을 찾아 가속하는 완벽 가이드

🚀 느린 파이썬 코드, 어디서 시간을 잡아먹을까? 프로파일링으로 해답 찾기

파이썬으로 서비스를 개발하다 보면 어느 순간 프로그램의 실행 속도가 발목을 잡는다는 느낌을 받게 될 때가 많습니다.

“분명 코드가 간단한데 왜 이렇게 느리지?”라는 고민에 빠지게 되죠.

아무리 직감적으로 느린 부분을 고친다고 해도, 실제로 병목 현상이 발생하는 ‘핫스폿(Hotspot)’을 정확히 찾아내지 못하면 시간 낭비일 뿐입니다.

오늘 글은 더 이상 감으로 성능을 추측하지 않고, 파이썬 표준 라이브러리인 cProfilepstats를 활용해 비효율적인 코드를 과학적으로 분석하고 극적으로 가속하는 방법을 알려드립니다.

파이썬 성능 최적화의 첫걸음은 ‘프로파일링(Profiling)’입니다.

프로파일링은 코드가 실행되는 동안 각 함수가 얼마나 많은 시간을 소요하는지 정밀하게 측정하는 과정으로, 우리가 집중적으로 개선해야 할 포인트를 명확히 제시해 줍니다.

특히 대규모 애플리케이션에서는 개발자가 예상하지 못한 의외의 함수가 성능 저하의 주범인 경우가 많습니다.

cProfile로 데이터를 수집하고, pstats의 강력한 필터링 및 정렬 기능을 사용하면 몇 줄의 명령만으로 프로그램 전체의 실행 흐름을 파악하고 최적화 기회를 포착할 수 있습니다.

이 글에서 runctx 사용법부터 시작해 strip_dirs()로 깔끔한 결과를 얻고, tottimecumtime을 기준으로 핫스폿을 찾아내는 실전 노하우를 상세히 다루겠습니다.

자, 이제 여러분의 파이썬 코드를 로켓처럼 빠르게 만들 준비가 되셨나요?



💻 cProfile과 pstats: 파이썬 성능 분석의 기본 도구 이해

파이썬 성능 최적화의 핵심은 정확한 진단에서 시작하며, 이때 사용되는 가장 강력한 표준 라이브러리가 바로 cProfilepstats입니다.

이 두 모듈은 서로 보완적인 역할을 수행하며, 파이썬 코드가 실행되는 방식에 대한 깊이 있는 통찰력을 제공합니다.

cProfile의 역할: 정확한 데이터 수집기

cProfile은 C 언어로 구현된 프로파일링 도구입니다.

C로 구현되었기 때문에 순수 파이썬으로 작성된 profile 모듈보다 훨씬 낮은 오버헤드로 정확한 성능 데이터를 수집할 수 있다는 큰 장점이 있습니다.

프로파일링을 위해 cProfile을 사용하면, 실행된 모든 함수 호출에 대해 다음 정보를 기록합니다.

  • 🔢함수 호출 횟수 (ncalls): 해당 함수가 총 몇 번 호출되었는지 기록합니다.
  • ⏱️함수 총 실행 시간 (cumtime): 함수와 그 함수가 호출한 모든 하위 함수들이 실행되는 데 걸린 누적 시간입니다.
  • 함수 자체 실행 시간 (tottime): 함수 내부에서만 소비된 시간으로, 하위 함수 호출 시간은 제외됩니다. 진짜 병목 지점을 찾는 핵심 지표입니다.

pstats의 역할: 프로파일링 결과 해석 및 보고서 생성

cProfile이 데이터를 수집하는 도구라면, pstats는 그 결과를 사람이 이해하기 쉽도록 가공하고 정렬하는 분석 도구입니다.

cProfile만으로는 방대한 raw 데이터만 얻게 되는데, 이 데이터를 효율적으로 탐색하기 위해 pstats.Stats 클래스를 사용합니다.

이 모듈의 진정한 힘은 sort_stats()strip_dirs() 같은 강력한 메서드에서 나옵니다.

수천 개의 함수 호출 중에서 가장 시간이 오래 걸린 상위 10개 함수만 골라낸다거나, 특정 모듈에서 발생한 호출만 필터링하는 등의 복잡한 분석 작업을 간단하게 처리할 수 있습니다.

실제 최적화 작업에서는 cProfile로 데이터를 저장하고, pstats를 인터랙티브하게 사용하며 핫스폿을 찾아내는 워크플로우가 가장 효율적입니다.

💡 TIP: 파이썬 2.x에서는 cProfile이 더 빠르다는 이점이 있었지만, 현재 파이썬 3 환경에서는 profile은 사실상 레거시 도구로 취급됩니다.

안정성과 속도 면에서 cProfile 사용이 표준이자 최선의 선택입니다.

⚙️ runctx()를 활용한 특정 코드 블록 프로파일링

일반적으로 파이썬 스크립트 전체를 프로파일링하는 방법(python -m cProfile script.py)도 있지만, 실제로는 전체 실행 시간 중 특정 함수나 코드 블록만 집중적으로 분석해야 할 때가 훨씬 많습니다.

이때 cProfile 모듈의 runctx() 메서드는 엄청나게 유용합니다.

runctx(command, globals, locals, filename) 함수는 문자열로 된 파이썬 코드(command)를 실행하면서 동시에 프로파일링 데이터를 수집하도록 설계되었습니다.

runctx()의 인자 이해하기: Context가 핵심

runctx()의 이름에서 알 수 있듯이, 이 함수는 실행할 코드 문자열 외에도 두 가지 중요한 인자를 받습니다.

  • 🌐globals: command 문자열이 실행될 때 사용할 전역 이름 공간(Global Namespace)입니다.
  • 📍locals: command 문자열이 실행될 때 사용할 지역 이름 공간(Local Namespace)입니다.
  • 💾filename: 프로파일링 결과를 저장할 파일 경로입니다. 이 결과는 나중에 pstats로 분석됩니다.

대부분의 경우 globals에는 globals()를, locals에는 locals()를 전달하여 현재 실행 중인 환경의 변수들을 사용할 수 있도록 합니다.

runctx() 실전 예제 코드

아래 코드는 cProfileProfiler 클래스를 인스턴스화하고 runctx()를 사용해 특정 함수 main_process()만 프로파일링한 후 결과를 파일로 저장하는 표준적인 방법입니다.

CODE BLOCK
import cProfile
import time

def slow_function(n):
    # 일부러 느리게 만드는 코드
    time.sleep(n / 1000)
    return n * 2

def main_process():
    total = 0
    for i in range(100):
        total += slow_function(i)
    return total

# Profiler 객체 생성
pr = cProfile.Profile()
# main_process() 함수를 문자열로 전달하고 현재 컨텍스트(globals/locals)를 사용
# 결과를 'profiling_result.prof' 파일에 저장합니다.
pr.runctx('main_process()', globals(), locals(), 'profiling_result.prof')

이렇게 runctx()를 사용하면, cProfile을 실행하기 위한 초기화 과정이나 파일 I/O 등 분석과 무관한 코드의 실행 시간을 제외하고 순수하게 목표 함수가 실행되는 동안의 데이터만 깨끗하게 얻을 수 있습니다.

이는 결과 해석을 훨씬 명확하고 효율적으로 만들어 줍니다.



📁 Stats.strip_dirs(): 프로파일링 결과 경로 정리 및 시각적 개선

프로파일링 데이터를 수집한 후 pstats로 분석을 시작하면, 출력되는 결과가 너무 지저분해서 당황할 때가 많습니다.

각 함수 호출 옆에 파일 이름과 함수 이름이 나타나는데, 이때 파일 이름에 전체 절대 경로가 포함되어 출력되기 때문입니다.

예를 들어, /Users/user/project/src/module/sub.py:100(function_name) 와 같은 형태가 출력되면 가독성이 현저히 떨어지고 핵심 정보를 파악하기 어렵습니다.

strip_dirs()의 기능: 불필요한 경로 제거

pstats.Stats 객체에 포함된 strip_dirs() 메서드는 이 문제를 해결하기 위해 존재합니다.

이 메서드는 모든 함수 호출 정보에서 파일 경로 부분을 제거하고, 순수한 파일명과 함수 이름만 남도록 데이터 구조를 정리해 줍니다.

위의 예시가 sub.py:100(function_name)처럼 간결하게 바뀌게 되어, 터미널 출력 결과가 훨씬 깔끔하고 분석하기 쉬워집니다.

💬 주의할 점은 strip_dirs()는 원본 Stats 객체를 직접 수정(In-place modification)한다는 것입니다. 따라서 한 번 호출하면 되돌릴 수 없으므로 보통 프로파일링 결과 분석의 가장 첫 단계로 실행합니다.

strip_dirs() 적용 예시

이전 단계에서 저장한 profiling_result.prof 파일을 로드하고 strip_dirs()를 적용하는 과정입니다.

CODE BLOCK
import pstats

# 1. pstats.Stats 객체 생성 (데이터 로드)
stats = pstats.Stats('profiling_result.prof')

# 2. strip_dirs()를 사용해 모든 경로 정보 제거
stats.strip_dirs()

# 3. 정리된 결과를 출력 (sort_stats는 다음 단계에서 자세히 다룹니다)
stats.sort_stats('tottime').print_stats(10) # 상위 10개 함수만 출력

결과적으로 파일 경로가 깔끔하게 정리되어 오로지 파일 이름과 함수 이름, 그리고 중요한 시간 지표들만 남게 됩니다.

이러한 사전 정리 작업은 수많은 함수 호출 정보를 다뤄야 하는 대규모 프로젝트의 프로파일링 분석에서 가독성을 획기적으로 개선하는 필수적인 단계입니다.

⏱️ sort_stats()로 핫스폿(Hotspot) 확인: ‘tottime’ vs ‘cumtime’

프로파일링 데이터를 수집하고 정리했다면, 이제 가장 중요한 작업인 핫스폿(Hotspot)을 찾아내야 합니다.

핫스폿은 전체 실행 시간의 대부분을 소비하는 특정 함수를 의미하며, pstats.Stats 객체의 sort_stats() 메서드가 이 작업을 도와줍니다.

최적화의 기준: ‘tottime’과 ‘cumtime’의 차이

sort_stats() 메서드는 정렬 기준을 인수로 받는데, 성능 최적화에서 가장 핵심이 되는 기준은 'tottime''cumtime'입니다.

이 두 지표의 의미를 정확히 이해해야 어떤 함수를 최적화해야 할지 결정할 수 있습니다.

지표 설명 최적화 포인트
tottime (Total Time) 함수 자체 내에서 소비된 시간 (하위 호출 시간 제외). 함수 내부의 로직 자체를 개선해야 할 때 (진짜 핫스폿).
cumtime (Cumulative Time) 함수 실행 시간 + 모든 하위 호출 함수들의 누적 시간. 흐름 제어 또는 전체 프로세스를 개선해야 할 때 (흐름 제어).

tottime으로 정렬: 즉각적인 병목 지점 찾기

대부분의 성능 최적화는 tottime이 가장 높은 함수에 집중하는 것으로 시작합니다.

이 함수가 하위 함수 호출 없이, 오직 자신만의 코드를 실행하는 데 가장 많은 시간을 소비했기 때문입니다.

만약 tottime이 높은 함수를 발견했다면, 해당 함수의 알고리즘이나 구현 방식을 변경하여 성능을 개선해야 합니다.

CODE BLOCK
# tottime 기준 상위 10개 함수 출력
stats.sort_stats('tottime').print_stats(10)

cumtime으로 정렬: 호출 흐름 파악하기

cumtime이 가장 높은 함수는 주로 최상위 레벨의 함수나 메인 프로세스 함수인 경우가 많습니다.

이 함수가 전체 실행 시간의 시작과 끝을 담당하고, 모든 하위 함수 호출 시간을 포함하기 때문입니다.

만약 최상위 함수가 아닌데도 cumtime이 높다면, 그 함수가 자주 호출되거나 비효율적인 하위 함수를 많이 호출하고 있다는 의미이므로 호출 구조를 검토할 필요가 있습니다.

CODE BLOCK
# cumtime 기준 상위 10개 함수 출력
stats.sort_stats('cumtime').print_stats(10)

💡 TIP: sort_stats는 여러 기준을 동시에 지정할 수 있습니다. 예를 들어, stats.sort_stats('tottime', 'ncalls')tottime으로 1차 정렬한 후, 시간이 같을 경우 호출 횟수(ncalls)로 2차 정렬합니다.



🔥 핫스폿 분석의 심화: 스택 병합과 그래프 시각화

단순히 tottime으로 상위 10개 함수를 보는 것만으로는 복잡하게 얽힌 함수 호출 관계를 이해하기 어렵습니다.

성능 분석을 진정으로 심화하려면, 함수가 어떤 경로로 호출되었는지, 즉 ‘호출 스택(Call Stack)’의 흐름을 파악하는 것이 필수적입니다.

호출 관계 파악: print_callers와 print_callees

pstats 모듈은 함수 간의 호출 관계를 텍스트 기반으로 확인할 수 있는 두 가지 강력한 메서드를 제공합니다.

  • ➡️print_callers(): 특정 함수를 호출한 상위 함수(호출자) 목록을 보여줍니다. 이 함수가 어떤 부모 함수 때문에 느려졌는지 파악할 때 유용합니다.
  • ⬅️print_callees(): 특정 함수가 호출한 하위 함수(피호출자) 목록을 보여줍니다. 이 함수가 어떤 자식 함수를 반복 호출해서 시간이 오래 걸렸는지 파악할 때 유용합니다.

이러한 정보는 특히 cumtime이 높게 나온 함수를 분석할 때, 실제 최적화가 필요한 지점이 해당 함수 자체가 아니라 하위의 다른 함수에 있다는 사실을 알려줄 수 있습니다.

불꽃 그래프(Flame Graph)를 이용한 시각화

텍스트 기반의 분석은 한계가 있습니다.

현대적인 성능 분석에서는 호출 스택을 시각적으로 나타내는 불꽃 그래프(Flame Graph)가 가장 강력한 도구로 손꼽힙니다.

불꽃 그래프는 cProfile 데이터를 기반으로 하며, 함수의 호출 스택을 시각적으로 병합하여 보여줍니다.

가로축은 시간(샘플 수)을 나타내고, 세로축은 호출 스택의 깊이를 나타냅니다.

불꽃 그래프에서 폭이 가장 넓은 ‘불꽃’이 바로 가장 많은 시간을 소비한 핫스폿과 그 호출 경로를 의미하므로, 직관적으로 병목 현상을 파악하고 최적화 대상을 결정할 수 있습니다.

💡 TIP: 파이썬 pstats 데이터를 불꽃 그래프로 변환하려면 snakevizpyinstrument와 같은 외부 라이브러리가 필요합니다. 특히 snakevizcProfile 파일을 웹 기반 대화형 시각화로 깔끔하게 변환해 주어 널리 사용됩니다.

자주 묻는 질문 (FAQ)

cProfile을 사용하면 왜 코드 실행 속도가 더 느려지나요?
프로파일링은 함수 호출 시마다 시간 측정 및 데이터 기록이라는 추가 작업을 수행하기 때문에 필연적으로 오버헤드가 발생합니다. cProfile은 C로 구현되어 파이썬 자체 프로파일러보다 오버헤드가 적지만, 정확한 측정 기록을 위해 실제 실행 속도보다는 느려지는 것이 정상입니다.
cProfile은 멀티스레딩이나 멀티프로세싱 환경에서도 정확한가요?
cProfile은 기본적으로 싱글 스레드 환경에 최적화되어 있습니다. 멀티스레딩 환경에서는 GIL(Global Interpreter Lock) 때문에 병렬성이 제한되므로 어느 정도 분석이 가능하지만, 멀티프로세싱 환경에서는 프로세스 간 컨텍스트 전환이 추적되지 않아 정확도가 떨어집니다. 이 경우 yappi 같은 멀티스레드/멀티프로세스 지원 프로파일러 사용이 권장됩니다.
tottime이 0.000인 함수는 무시해도 되나요?
네, tottime이 0이거나 매우 작은 함수는 해당 함수 자체의 로직이 빠르다는 의미이므로 최적화 대상에서 제외해도 됩니다. 다만, 해당 함수의 cumtime이 높다면, 그 함수가 느린 하위 함수를 ‘많이’ 호출하고 있을 가능성이 있으므로 print_callees()로 호출 관계를 추가 분석해볼 수 있습니다.
pstats 분석 시 print_stats(10) 대신 10을 인수로 사용하는 이유는 무엇인가요?
print_stats()의 인수는 출력할 함수 목록의 ‘개수’ 또는 ‘정규 표현식’을 지정합니다. 10을 인수로 사용하면 정렬된 결과 중에서 상위 10개의 함수만 출력하게 되어, 가장 시간을 많이 소비하는 핫스폿만 빠르게 파악할 수 있도록 돕습니다.
프로파일링 데이터를 저장하지 않고 바로 터미널에 출력할 수도 있나요?
네, cProfile.run('code') 함수를 사용하면 결과를 파일로 저장하지 않고 표준 출력(터미널)에 바로 출력할 수 있습니다. 하지만 run()pstats 객체에 직접 접근하여 strip_dirs()sort_stats() 같은 고급 분석 기능을 사용할 수 없다는 단점이 있습니다.
파이썬 표준 라이브러리 외에 추천하는 프로파일링 도구가 있나요?
snakevizcProfile 결과를 대화형 웹 인터페이스(선버스트 차트)로 시각화해 주는 강력한 도구입니다. 또한, pyinstrument는 샘플링 기반 프로파일링으로 오버헤드가 매우 적으며, 사용하기 쉬운 출력 보고서를 제공하여 초보자에게 유용합니다.
runctx()를 사용하지 않고 Decorator를 이용해 특정 함수만 프로파일링할 수 있나요?
네, 맞습니다. cProfile이나 profile을 직접 사용하는 대신, 프로파일링을 시작하고 종료하는 로직을 담은 커스텀 데코레이터(@profile_me 등)를 만들어 특정 함수에만 붙여서 간편하게 프로파일링할 수 있습니다. 이는 코드의 가독성을 높이고 테스트 환경에서 유용합니다.
파이썬에서 I/O 작업이 많은 코드를 프로파일링할 때 주의할 점은 무엇인가요?
I/O 작업(네트워크 통신, 디스크 파일 읽기/쓰기 등)은 대부분의 시간을 데이터가 오기를 기다리는 ‘대기 시간(Waiting Time)’으로 보냅니다. cProfile은 이 대기 시간까지 모두 함수 실행 시간(tottime)에 포함합니다. 따라서 I/O 바운드 작업에서는 실제 CPU 사용 시간만 측정하는 도구(예: time.perf_counter()와 비교)나, py-spy 같은 샘플링 프로파일러가 더 유용할 수 있습니다.

💾 cProfile/pstats 기반 성능 최적화 마스터 로드맵

파이썬 코드의 성능 문제는 직관이나 추측으로 해결할 수 없습니다.

오늘 소개한 cProfilepstats는 파이썬 표준 라이브러리임에도 불구하고, 프로그램의 병목 현상(핫스폿)을 과학적으로 진단하고 극복할 수 있는 가장 확실한 기반을 제공합니다.

핵심은 cProfilerunctx()로 원하는 코드 블록만 선택적으로 프로파일링 데이터를 수집하는 것, 그리고 pstats로 데이터를 로드한 후 strip_dirs()로 가독성을 높이고 sort_stats('tottime')으로 실제 병목 함수를 찾아내는 일련의 과정입니다.

특히 ‘tottime’이 높은 함수는 해당 함수 내부의 알고리즘을 개선하거나 더 효율적인 파이썬 내장 함수를 사용하는 방식으로 최적화해야 합니다.

만약 분석 결과가 복잡하다면, snakeviz와 같은 외부 도구를 활용해 불꽃 그래프(Flame Graph) 형태로 시각화하여 호출 스택과 시간을 한눈에 파악하는 것이 좋습니다.

성능 최적화는 단순히 코드를 빠르게 만드는 것을 넘어, 리소스 사용 효율을 높이고 더 안정적인 소프트웨어를 만드는 데 기여하는 중요한 개발 역량입니다.

오늘 배운 내용을 바탕으로 여러분의 파이썬 프로젝트에서 잠재적인 성능 향상 포인트를 찾아내고, 최적화를 통해 로켓처럼 빠른 코드를 구현하시길 응원합니다.


🏷️ 관련 태그 : 파이썬성능최적화, cProfile, pstats, tottime, cumtime, 파이썬프로파일링, 핫스폿분석, 파이썬가속, strip_dirs, sort_stats