Python Optimization은 이 한 단계를 건너뛰면 실패한다

발행: (2025년 12월 20일 오후 03:35 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

대부분의 개발자가 직면하는 문제

당신은 파이썬 스크립트가 느리다는 것을 눈치챕니다. 최적화에 관한 글을 읽어봤습니다. 리스트 컴프리헨션이 루프보다 빠르다는 것을 알고 있습니다. NumPy가 빠르다는 얘기도 들어봤습니다. 그래서 코드를 다시 작성하기 시작합니다.

문제는 이렇습니다: 당신은 데이터를 기반으로 하지 않고 가정에 따라 최적화를 시도했습니다.

모두가 건너뛰는 한 단계

최적화 전에 프로파일링하세요.
모두가 그렇게 말하지만, 대부분의 개발자(과거의 나 포함)는 바로 최적화 단계로 넘어갑니다.

접근 방식을 바꾼 실제 예시

def process_sales_data(filename):
    df = pd.read_csv(filename)
    # Calculate profit for each row
    profits = []
    for index, row in df.iterrows():
        profit = row['revenue'] - row['cost']
        profits.append(profit)
    df['profit'] = profits

    # Filter and group
    profitable = df[df['profit'] > 100]
    averages = profitable.groupby('region')['profit'].mean()

    return averages

100,000개의 행을 처리하는 데 42초가 걸렸습니다 – 너무 느립니다.

프로파일링 결과

import cProfile
import pstats
from io import StringIO

profiler = cProfile.Profile()
profiler.enable()

process_sales_data('sales.csv')

profiler.disable()
stream = StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats('cumulative')
stats.print_stats(20)

print(stream.getvalue())

출력 결과에 충격을 받았습니다:

  • iterrows() 루프가 **90 %**의 실행 시간을 차지했습니다.
  • 걱정했던 groupby는 **2 %**에 불과했습니다.

해결 방법은 간단했습니다

def process_sales_data(filename):
    df = pd.read_csv(filename)

    # Vectorized operation – no loop
    df['profit'] = df['revenue'] - df['cost']

    # Same filtering and grouping
    profitable = df[df['profit'] > 100]
    averages = profitable.groupby('region')['profit'].mean()

    return averages

런타임이 42 초에서 1.2 초로 감소했으며, 세 줄만 바꾼 것만으로 35배의 속도 향상을 달성했습니다.

왜 우리의 직관이 실패하는가

  • 우리는 구문에 집중하고 실행 비용을 놓친다. 중첩 루프는 느려 보이지만 10개의 항목에 한 번만 실행된다면 무시할 수 있다. 100,000개의 항목을 처리하는 단일 함수 호출이 더 중요하다.
  • 우리는 파이썬의 오버헤드를 과소평가한다. pandas에서 행‑단위 반복은 엄청난 오버헤드를 만든다; 간단해 보이는 것이 수천 번의 연산을 트리거한다.
  • 우리는 최근 변경이 느려짐을 일으켰다고 가정한다. 종종 느려짐은 원래부터 있었지만 데이터 크기가 커질 때까지 눈에 띄지 않았다.
  • 우리는 이해하는 부분을 최적화한다. 나는 그룹화 연산을 이해했기에 그쪽에 집중했다. 실제 문제는 내가 고려하지 않았던 부분에 있었다.

올바르게 프로파일링하기

import cProfile
import pstats

profiler = cProfile.Profile()
profiler.enable()

your_function()

profiler.disable()

stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')  # Sort by total time including calls
stats.print_stats(20)           # Show top 20 functions

먼저 cumtime 열을 확인하세요. 이는 중첩 호출을 포함한 전체 시간입니다. cumtime이 가장 높은 함수가 주요 대상이 됩니다.

내가 지금 따르는 패턴

  1. 기본 성능 측정 – 전체 작업에 걸리는 시간을 측정합니다.
  2. 병목 현상 찾기 위해 프로파일링 – 추측이 아니라 cProfile을 사용합니다.
  3. 가장 큰 병목 현상 최적화 – 실제로 시간을 소비하는 부분을 수정합니다.
  4. 다시 측정 – 개선 사항을 검증합니다.
  5. 필요하면 반복 – 다시 프로파일링하여 다음 병목 현상을 찾습니다.

이 체계적인 접근법은 매번 직관보다 뛰어납니다.

일반적인 프로파일링 발견

  • 데이터베이스 쿼리가 실행 시간의 80 % 이상을 차지합니다 (Python 코드가 아니라 쿼리를 최적화하세요).
  • 파일 I/O가 데이터‑처리 스크립트의 대부분을 차지합니다 (버퍼링 작업을 사용하고, 바이너리 형식을 활용하세요).
  • pandas에서 행‑단위 반복은 막대한 오버헤드를 발생시킵니다 (모두 벡터화하세요).
  • 루프 안에서 문자열을 연결하면 O(n²) 복잡도가 발생합니다 (''.join()을 사용하세요).
  • 불필요한 객체 생성을 하면 가비지 컬렉션 부담이 증가합니다 (버퍼를 재사용하세요).

코드를 읽는 것만으로는 이러한 문제를 찾을 수 없습니다. 프로파일링을 통해 발견하게 됩니다.

교훈

Python 코드를 최적화하기 위해 시간을 들이기 전에:

  • 블로그 글을 근거로 작동하는 코드를 다시 작성하지 마세요.
  • 느린 부분을 스스로 추정하지 마세요.
  • 여러 가지를 동시에 최적화하려 하지 마세요.
  • 측정을 건너뛰지 마세요.

먼저 프로파일링하세요. 언제나.


Python 최적화에 대해 더 깊이 파고들고 싶나요? 포괄적인 가이드를 확인해 보세요: Python Optimization Guide: How to Write Faster, Smarter Code.

Back to Blog

관련 글

더 보기 »

Python의 `os`에서 공백이 포함된 Windows 경로

파일 경로에 공백이 포함된 경우 처리하기 파일 경로에 공백이 있을 때, 문자열이 Python에서 올바르게 해석되도록 해야 합니다. 다음 중 하나를 선택할 수 있습니다: - raw 문자열 사용...