LLM 기반 서비스를 개발하다 보면 한 번쯤 이런 상황을 만납니다. 사용자가 질문을 입력했는데 응답까지 15초가 걸립니다. 느리다는 건 알겠는데 왜 느린지는 모르는 상황입니다. LLM이 문제일까요, 벡터 DB 검색이 문제일까요, 아니면 다른 곳일까요?
이럴 때 필요한 것이 프로파일링(Profiling) 입니다. 이 글에서는 프로파일링이 무엇인지, 그리고 실무에서 자주 사용하는 두 도구인 py-spy와 pyinstrument를 어떤 상황에서 어떻게 활용하는지 정리합니다.
프로파일링이란 무엇인가
프로파일링은 프로그램이 실행되는 동안 시간과 자원이 어디에서 얼마나 소비되는지 측정하는 과정입니다. 단순히 전체 실행 시간을 재는 것이 아니라, 어느 함수가 얼마나 오래 실행됐는지, CPU를 얼마나 사용했는지, 어디서 대기가 발생하는지를 함수 단위로 파악합니다.
디버깅과 혼동하기 쉬운데 둘은 목적이 다릅니다. 디버깅은 프로그램이 올바르게 동작하지 않는 원인을 찾는 과정이고, 프로파일링은 프로그램은 정상적으로 동작하지만 왜 느린지, 어디가 성능 병목(Bottleneck)인지를 찾는 과정입니다.
병원에 비유하면 이렇습니다. 프로젝트는 사람의 몸이고, FastAPI나 LangGraph 같은 각 컴포넌트는 심장이나 폐 같은 장기입니다. 프로파일링 도구는 그 장기들을 들여다보는 CT나 MRI 장비입니다. CT가 몸의 일부가 아닌 것처럼, 프로파일링 도구도 서비스를 구성하는 컴포넌트가 아니라 전체를 관찰하는 진단 도구입니다.
왜 cProfile 대신 다른 도구를 쓰는가
Python에는 표준 프로파일러인 cProfile이 내장되어 있습니다. 그런데 실무에서는 py-spy나 pyinstrument를 더 많이 사용합니다.
cProfile의 가장 큰 한계는 운영 서버에서 사용하기 어렵다는 점입니다. 코드를 수정해야 하고, 프로그램을 재실행해야 하며, 이미 돌아가고 있는 프로세스를 분석할 수 없습니다. 반면 py-spy는 실행 중인 프로세스에 외부에서 붙어서 분석할 수 있어 운영 서버에서도 사용이 가능합니다. pyinstrument는 결과를 함수 호출 트리(Call Tree) 형태로 보여줘서 병목을 한눈에 파악하기 쉽습니다.
py-spy: 운영 서버의 상태를 실시간으로 들여다보기
py-spy는 실행 중인 Python 프로세스를 외부에서 관찰하는 프로파일러입니다. 코드를 수정하거나 서비스를 중단하지 않아도 되기 때문에 운영 환경에서 장애가 발생했을 때 특히 유용합니다.
py-spy top 명령어는 현재 각 함수가 CPU를 얼마나 사용하고 있는지를 실시간으로 보여줍니다. 아래처럼 프로세스 ID(PID)를 지정하면 됩니다.
# 실행 중인 Python 프로세스의 PID 확인
ps aux | grep python
# py-spy로 해당 프로세스 분석 (PID: 12345 예시)
py-spy top --pid 12345
실행하면 다음과 같은 결과를 볼 수 있습니다.
Function CPU 사용률
call_openai() 70%
embedding() 12%
json.dumps() 5%
redis.get() 3%
others 10%
py-spy dump 명령어를 사용하면 현재 프로그램이 어느 함수에서 멈춰 있는지 호출 스택을 확인할 수 있습니다. waiting... 상태가 보이면 그 지점이 대기 구간입니다.
py-spy dump --pid 12345
Thread 1
graph.invoke()
└── agent()
└── tool()
└── call_openai()
└── waiting...
py-spy record 명령어는 Flame Graph(불꽃 그래프)를 생성합니다. 가로 폭이 넓을수록 그 함수가 실행 시간을 많이 차지했다는 의미입니다.
call_openai() ████████████████████
embedding() █████████
json.dumps() ███
py-spy는 응급실 모니터와 비슷합니다. 이미 움직이고 있는 환자를 멈추지 않고 실시간으로 상태를 확인하는 것처럼, 운영 중인 서비스를 중단하지 않고 어디에서 시간이 소비되는지 관찰할 수 있습니다.
pyinstrument: 개발 중 함수별 실행 시간을 트리로 확인하기
pyinstrument는 Python 코드 내부에서 함수가 얼마나 오래 실행됐는지를 트리 형태로 보여주는 프로파일러입니다. 코드에 몇 줄을 추가해야 하지만, 그 결과가 사람이 읽기 쉬운 형태로 출력된다는 것이 가장 큰 장점입니다.
다음은 LangGraph 그래프 실행을 프로파일링하는 예시입니다.
from pyinstrument import Profiler
profiler = Profiler()
# 프로파일링 시작
profiler.start()
result = graph.invoke(question)
# 프로파일링 종료 및 결과 출력
profiler.stop()
print(profiler.output_text())
출력 결과는 이런 형태입니다.
Duration: 8.41 seconds
graph.invoke() 8.41s
├── retrieve_documents() 0.15s
├── search_vector_db() 0.10s
├── call_openai() 6.90s
├── parse_response() 0.90s
└── make_json() 0.36s
call_openai()가 전체 8.41초 중 6.90초를 차지하고 있다는 것을 한눈에 알 수 있습니다. pyinstrument는 건강검진을 예약해서 차분히 받는 정밀검사와 비슷합니다. 개발 환경에서 코드를 실행하며 어떤 함수가 얼마나 시간을 사용하는지 상세하게 확인할 수 있습니다.
프로파일링 결과를 어떻게 해석할 것인가
중요한 점은, 가장 오래 걸린 부분을 무조건 최적화하는 것이 아니라는 점입니다. 그 시간이 정상 범위인지 먼저 판단해야 합니다.
예를 들어 위 결과에서 call_openai()가 6.90초 걸렸다고 해서 바로 "LLM이 느리니까 바꾸자"가 아닙니다. LLM API 호출은 원래 몇 초가 걸리는 것이 정상일 수 있습니다. 대신 이런 질문들을 던져야 합니다.
- 프롬프트에 검색 결과를 너무 많이 넣고 있지는 않은가?
- 더 작은 모델로 대체할 수 있는가?
- 스트리밍(Streaming, LLM이 생성하는 즉시 조금씩 응답을 보내는 방식)을 적용해 체감 속도를 개선할 수 있는가?
- 동일한 요청이 반복된다면 캐싱을 적용할 수 있는가?
병목의 원인이 다르면 개선 방법도 달라집니다.
| 병목 구간 | 적용 가능한 개선 방법 |
| Vector DB 검색 | 인덱스 최적화 |
| 외부 API 호출 | 비동기 처리 |
| 동일한 요청 반복 | 캐싱 적용 |
| DB 조회 | 쿼리 최적화 |
| CPU 연산 집중 | 멀티프로세싱 |
프로파일링은 병목이 어디에 있는지 알려주고, 최적화는 그 원인에 따라 다르게 적용하는 것입니다.
두 도구의 차이와 사용 시점
| 항목 | py-spy | pyindustrument |
| 실행 방식 | 외부에서 프로세스 관찰 | 코드 내부에서 실행 |
| 코드 수정 필요 여부 | 필요 없음 | 몇 줄 추가 필요 |
| 운영 서버 사용 | 매우 적합 | 개발 환경 권장 |
| Flame Graph 지원 | 지원 | 미지원 (HTML 트리 중심) |
| 실행 중 프로세스 분석 | 가능 | 불가능 |
| 추천 상황 | 운영 장애 분석 | 개발 중 성능 개선 |
실무에서 두 도구는 각 단계에 맞게 나눠서 사용합니다.
개발 단계에서 기능을 완성한 후 응답 속도를 확인할 때는 pyinstrument로 함수별 실행 시간을 파악하고 병목을 수정합니다. 배포 후 운영 중 사용자 증가나 장애가 발생했을 때는 서비스를 중단하지 않고 py-spy로 원인을 파악합니다.
마치며
- 프로파일링은 프로그램이 정상 동작하는 상황에서 시간과 자원이 어디에서 소비되는지 측정해 성능 병목을 찾아내는 과정이며, 기능 오류를 찾는 디버깅과는 목적이 다릅니다.
- py-spy는 코드 수정 없이 실행 중인 프로세스를 분석할 수 있어 운영 서버 장애 대응에 적합하고, pyinstrument는 함수 호출 트리를 통해 개발 중 병목을 빠르게 파악하는 데 적합합니다.
- 프로파일링 결과에서 가장 오래 걸린 구간을 무조건 최적화하는 것이 아니라, 그 시간이 정상 범위인지 판단한 후 원인에 맞는 개선 방법(캐싱, 비동기 처리, 쿼리 최적화 등)을 선택하는 것이 실무적인 접근입니다.
'Python' 카테고리의 다른 글
| [Python] sort(), reverse()정리 (0) | 2026.03.27 |
|---|---|
| [Python] Selenium에서의 innerHTML, innerText, textContent 차이점 (0) | 2026.03.18 |
| [Python] 정규표현식 (0) | 2026.03.13 |
| [Python] pytest를 이용한 테스트 코드 작성법 (0) | 2026.03.11 |
| [Python] json.dumps() 사용법, 사용이유 (1) | 2026.03.10 |