3-4 성능 최적화 전략

3-4 성능 최적화 전략

← 목차로 돌아가기

썸네일

파이썬은 개발 생산성이 뛰어나지만, 때로는 코드 실행 속도 때문에 고민할 때가 있습니다. 특히 대량의 데이터를 처리하거나 복잡한 연산을 수행할 때 ‘이 코드를 더 빠르게 만들 수는 없을까?’라는 질문을 자주 마주하게 되죠. 이번 장에서는 여러분의 파이썬 코드를 한 단계 더 효율적으로 만드는 실용적인 성능 최적화 전략들을 소개합니다.

왜 성능 최적화가 중요한가요?

코드 성능 최적화는 자원 효율성을 높이고 사용자 경험을 개선하는 핵심 과정입니다.

파이썬은 인터프리터 언어의 특성상 C++이나 Java 같은 컴파일 언어보다 기본적으로 느린 경향이 있습니다. 하지만 무조건 느린 것은 아닙니다. 어떤 코드 패턴을 사용하느냐, 어떤 라이브러리를 활용하느냐에 따라 성능은 천차만별로 달라질 수 있습니다. 저의 현업 경험을 돌아보면, 데이터 분석 스크립트 하나를 몇 시간 걸리던 것에서 몇 분으로 줄였을 때, 그 결과로 얻는 업무 효율성 향상은 정말 놀라웠습니다. 특히 자동화 스크립트나 백엔드 서비스처럼 반복적으로 실행되는 코드의 경우, 초기 단계에서 약간의 성능 최적화만으로도 장기적으로 엄청난 시간과 비용을 절약할 수 있습니다. 예를 들어, 웹 애플리케이션 개발 사례에서 보듯, FastAPI 같은 프레임워크로 API를 만들 때도 내부 로직의 최적화는 응답 속도에 직접적인 영향을 줍니다. 사용자 수가 많아질수록 이 차이는 더욱 극명해집니다.

성능 측정의 첫걸음: 프로파일링

어디가 문제인지 정확히 아는 것이 성능 최적화의 시작입니다.

코드를 무턱대고 수정하기 전에, ‘어느 부분이 가장 많은 시간을 잡아먹는지’를 파악하는 것이 중요합니다. 이를 프로파일링(Profiling)이라고 합니다. 파이썬은 이 과정을 위한 유용한 도구들을 내장하고 있습니다.

time 모듈로 간단하게 시간 재기

작은 코드 블록의 실행 시간을 측정할 때는 time 모듈이 가장 간편합니다.

특정 함수의 실행 시간을 빠르게 확인하고 싶을 때 time 모듈의 time() 함수를 활용할 수 있습니다. 이 함수는 현재 시각을 반환하므로, 함수 실행 전후의 시간을 측정하여 그 차이를 계산하면 됩니다.

import time

def my_slow_function():
    total = 0
    for i in range(1_000_000):
        total += i * i
    return total

start_time = time.time()  # 함수 시작 전 시간 기록
result = my_slow_function()
end_time = time.time()    # 함수 종료 후 시간 기록

print(f"함수 실행 결과: {result}")
print(f"함수 실행 시간: {end_time - start_time:.4f}초")

이 코드가 하는 일: my_slow_function이 실행되는 데 걸린 시간을 측정하여 출력합니다.

cProfile로 병목 현상 찾아내기

코드 전체에서 시간이 오래 걸리는 특정 함수나 라인을 찾아낼 때 cProfile이 강력합니다.

cProfile은 파이썬 프로그램의 각 함수가 얼마나 자주 호출되었고, 총 몇 초가 소요되었는지를 상세하게 알려주는 프로파일러입니다. 이를 통해 ‘병목 현상(Bottleneck)’이 발생하는 지점을 정확히 식별할 수 있습니다. 저의 경험상, 저는 데이터 처리 파이프라인을 짤 때 이 cProfile을 자주 활용하는데, 생각지도 못했던 작은 함수에서 시간을 많이 잡아먹고 있는 경우를 여러 번 발견했습니다. 성능 병목 현상 진단의 한 방법으로 cProfile은 매우 유용합니다.

import cProfile

def func_a():
    sum(range(10_000_000))

def func_b():
    [i * i for i in range(5_000_000)]

def main():
    func_a()
    func_b()

cProfile.run('main()')

이 코드가 하는 일: main 함수 내부에서 호출되는 각 함수의 실행 시간과 호출 횟수를 프로파일링하여 상세 보고서를 출력합니다. 보고서를 보면 ncalls(호출 횟수), tottime(함수 자체 실행 시간), cumtime(하위 함수 포함 총 실행 시간) 등의 정보를 파악할 수 있습니다.

코드 레벨 최적화 기법

작은 코드 변경만으로도 성능에 큰 영향을 미칠 수 있습니다.

프로파일링을 통해 병목 지점을 찾았다면, 이제 해당 코드 부분을 더 효율적으로 개선할 차례입니다.

자료 구조 선택의 중요성

상황에 맞는 자료 구조를 선택하는 것이 성능 향상의 기본입니다.

파이썬의 내장 자료 구조(리스트, 튜플, 셋, 딕셔너리)는 각각의 특성과 성능 이점을 가집니다. 예를 들어, 특정 요소가 컬렉션에 있는지 확인하는 작업(멤버십 테스트)에서는 리스트(list)보다 셋(set)이나 딕셔너리(dict)가 훨씬 빠릅니다. 셋과 딕셔너리는 해시 테이블 기반이라 평균적으로 O(1)의 탐색 시간을 가지지만, 리스트는 O(n)의 탐색 시간을 가집니다. 변수와 데이터 타입 다루기 장에서 다뤘듯, 데이터의 특성을 이해하고 적절한 자료 구조를 선택하는 것이 중요합니다.

import time

# 리스트에서 특정 요소 찾기
my_list = list(range(1_000_000))
start = time.time()
print(999999 in my_list)
print(f"List search time: {time.time() - start:.6f}초")

# 셋에서 특정 요소 찾기
my_set = set(range(1_000_000))
start = time.time()
print(999999 in my_set)
print(f"Set search time: {time.time() - start:.6f}초")

이 코드가 하는 일: 리스트와 셋에서 같은 요소를 찾는 시간을 비교하여 셋이 훨씬 빠르다는 것을 보여줍니다.

반복문 최적화: 리스트 컴프리헨션과 제너레이터

파이썬스러운 반복문 사용은 가독성과 성능을 동시에 개선합니다.

파이썬에서는 for 루프 대신 리스트 컴프리헨션(List Comprehension)이나 제너레이터(Generator) 표현식을 사용하여 코드를 더 간결하고 빠르게 만들 수 있습니다. 특히 리스트 컴프리헨션은 C로 구현된 내부 반복 로직을 사용하기 때문에 일반적인 for 루프보다 빠릅니다. 제너레이터는 모든 요소를 한 번에 메모리에 올리지 않고 필요할 때마다 생성하므로, 대용량 데이터를 다룰 때 메모리 효율성을 크게 높여줍니다.

import time

# 일반 for 루프
start = time.time()
result_for = []
for i in range(10_000_000):
    result_for.append(i * 2)
print(f"For loop time: {time.time() - start:.6f}초")

# 리스트 컴프리헨션
start = time.time()
result_comp = [i * 2 for i in range(10_000_000)]
print(f"List comprehension time: {time.time() - start:.6f}초")

# 제너레이터 (메모리 사용 효율적)
start = time.time()
gen_expr = (i * 2 for i in range(10_000_000)) # 이 시점에는 아직 계산 안함
# for _ in gen_expr: pass # 필요할 때만 값 생성 및 사용
print(f"Generator expression creation time: {time.time() - start:.6f}초")

이 코드가 하는 일: 일반 for 루프와 리스트 컴프리헨션의 실행 시간을 비교하고, 제너레이터 표현식의 생성 속도를 보여줍니다. 리스트 컴프리헨션이 더 빠르며, 제너레이터는 생성 자체는 즉각적이지만 값을 사용할 때 연산됩니다.

불필요한 연산 줄이기

같은 연산을 반복하지 않고 미리 계산하여 재사용하는 습관이 중요합니다.

반복문 안에서 동일한 값을 계속 계산하거나, 함수의 결과를 반복해서 호출하는 것은 비효율적입니다. 예를 들어, 반복문 조건이나 내부에서 변하지 않는 값을 미리 계산해두면 반복마다 불필요한 연산을 줄일 수 있습니다. 또한, 문자열 결합 시 + 연산 대신 .join() 메서드를 사용하는 것이 훨씬 효율적입니다. 파이썬 3.6부터는 f-string이 도입되어 가독성과 성능을 모두 잡을 수 있게 되었습니다. 저의 자동화 스크립트에서는 이러한 미세한 최적화가 전체 실행 시간을 몇 초씩 줄여주기도 합니다.

외부 라이브러리 활용: NumPy와 Cython

고성능 라이브러리는 파이썬의 속도 한계를 극복하는 효과적인 수단입니다.

파이썬만으로는 도저히 성능을 끌어올리기 어려운 경우, C나 C++로 구현된 고성능 라이브러리를 활용하는 것이 현명한 방법입니다. 특히 수치 연산과 복잡한 알고리즘 구현하기에서 큰 강점을 보입니다.

NumPy를 활용한 수치 연산 가속

대규모 수치 데이터 처리에는 NumPy가 필수적입니다.

데이터 분석가와 과학 기술 분야에서 NumPy는 파이썬의 연산 속도를 혁신적으로 끌어올리는 핵심 라이브러리입니다. C로 구현된 벡터화된 연산을 제공하여 파이썬의 느린 반복문을 우회합니다. 제가 pandas로 대용량 데이터를 처리할 때 NumPy의 강력함을 항상 느끼곤 합니다. 예를 들어, 수백만 개의 숫자에 대해 같은 연산을 수행할 때 파이썬 리스트와 NumPy 배열의 속도 차이는 비교할 수 없을 정도입니다.

import numpy as np
import time

# 파이썬 리스트 연산
python_list = list(range(10_000_000))
start = time.time()
python_result = [x * 2 for x in python_list]
print(f"Python list multiplication: {time.time() - start:.6f}초")

# NumPy 배열 연산
numpy_array = np.arange(10_000_000)
start = time.time()
numpy_result = numpy_array * 2
print(f"NumPy array multiplication: {time.time() - start:.6f}초")

이 코드가 하는 일: 파이썬 리스트와 NumPy 배열의 요소별 곱셈 연산 시간을 비교하여 NumPy의 성능 우위를 보여줍니다. (2026년 기준, NumPy는 여전히 파이썬 수치 연산의 표준입니다.)

Cython으로 파이썬 코드 컴파일하기

성능이 중요한 파이썬 코드 조각을 C 언어처럼 컴파일하여 실행 속도를 높일 수 있습니다.

Cython은 파이썬 코드를 C 코드로 변환하고 이를 컴파일하여 파이썬 인터프리터를 거치지 않고 직접 실행할 수 있게 해주는 도구입니다. 특정 함수의 성능이 프로그램 전체의 병목 현상을 일으키고, NumPy 등으로도 해결이 안 될 때 고려해볼 만한 고급 최적화 기법입니다. 주로 연산 집약적인 부분에 적용하여 파이썬의 유연성과 C의 속도를 동시에 얻을 수 있습니다.

병렬 처리와 비동기 프로그래밍

멀티코어 CPU를 활용하거나 I/O 작업을 효율적으로 관리하여 성능을 향상시킬 수 있습니다.

단일 스레드(Single-threaded) 환경에서 파이썬의 GIL(Global Interpreter Lock)은 진정한 의미의 병렬 처리를 어렵게 합니다. 하지만 프로세스 기반의 멀티스레드 프로그래밍(정확히는 멀티프로세싱)을 사용하거나, 네트워크 요청처럼 I/O 바운드 작업에서는 비동기 프로그래밍(asyncio)을 통해 성능을 크게 개선할 수 있습니다. 저의 자동화 프로젝트 중에는 여러 웹사이트에서 동시에 데이터를 수집해야 하는 경우가 있었는데, 이때 requestsasyncio를 조합하여 비동기 처리를 구현해 처리 시간을 드라마틱하게 줄였습니다. 외부 API 연동 방법에서 다룬 내용과도 밀접하게 관련되어 있습니다.

최적화 시 고려할 점과 주의사항

모든 코드에 최적화를 적용할 필요는 없으며, 항상 측정 후에 판단해야 합니다.

조기 최적화의 함정

성능 문제가 발생하기도 전에 최적화를 시도하는 것은 시간 낭비가 될 수 있습니다.

‘시기상조의 최적화는 모든 악의 근원이다(Premature optimization is the root of all evil)’라는 말이 있습니다. 저 역시 초보 시절에는 모든 코드를 최대한 빠르게 만들려고 애썼습니다. 하지만 실제로는 프로그램의 90%는 10%의 시간만 소요하고, 나머지 10%의 코드가 90%의 시간을 잡아먹는 경우가 많습니다. 그러므로 항상 프로파일링을 통해 병목 현상이 발생하는 부분을 먼저 찾고, 그 부분을 집중적으로 개선하는 것이 효율적입니다. 효율적인 디버깅 기법과 마찬가지로, 문제의 원인을 정확히 아는 것이 중요합니다.

가독성과 유지보수성

성능 최적화는 가독성과 유지보수성을 희생해서는 안 됩니다.

과도한 최적화는 코드를 복잡하게 만들고 이해하기 어렵게 만들 수 있습니다. 특히 팀 프로젝트에서는 다른 동료들이 코드를 이해하고 수정하기 어렵게 만들어 생산성을 저해할 수 있습니다. 코드 문서화 표준이나 리팩토링 가이드라인에서 강조하듯, 명확하고 깔끔한 코드를 유지하는 것이 장기적으로 중요합니다. 성능 개선과 코드의 명확성 사이에서 균형을 찾는 것이 중요합니다. 때로는 약간의 성능 손실을 감수하고라도 더 읽기 쉬운 코드를 선택하는 것이 현명한 결정일 수 있습니다.

이 장에서 다룬 전략들을 바탕으로 여러분의 파이썬 코드를 더욱 강력하고 효율적으로 만들어보세요. 중요한 것은 ‘측정하고, 개선하고, 다시 측정하는’ 반복적인 과정입니다. 꾸준히 연습하여 나만의 최적화 노하우를 쌓아나가시길 바랍니다.

참고 자료

  1. [1] Python 공식 문서 — https://docs.python.org/ko/3/
  2. [2] Claude Code 공식 문서 — https://docs.claude.com/en/docs/claude-code/overview

#파이썬성능, #코드최적화, #프로파일링, #NumPy활용, #Cython, #비동기프로그래밍, #자료구조선택, #파이썬속도, #개발효율성, #병목현상

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다