배경
EcoCache 프로젝트에 /chat API를 붙이면서 탄소 배출량 모니터링 기능도 함께 통합했다. 매 요청마다 현재 탄소 집약도(Carbon Intensity, CI)를 가져와서 응답에 포함시키는 구조였는데, 어느 순간부터 API 응답이 굉장히 느려지기 시작했다.
처음엔 LLM 응답 생성이 느린 건가 싶었다. 근데 실제로 timings 필드를 찍어보니 이상한 곳에서 5초가 넘게 잡히고 있었다.

병목 1: Carbon API 호출
첫 번째 문제는 _get_current_ci() 함수였다. 매 /chat 요청마다 Electricity Maps 외부 API를 직접 호출하고 있었다. 네트워크 레이턴시가 합쳐지면서 운이 나쁘면 몇 초씩 걸렸다.
구조를 바꿨다. carbon/collector.py가 15분마다 CI 값을 PostgreSQL에 저장하도록 이미 되어 있었으니, /chat API는 그 DB에서 최신 값을 읽기만 하면 됐다.
# 변경 전: 매 요청마다 외부 API 직접 호출
def _get_current_ci() -> float | None:
try:
from carbon_optimizer import get_optimizer
return get_optimizer().get_current_ci()
except Exception:
return None
# 변경 후: DB에서 먼저 읽고, 없으면 fallback
# (get_latest_ci_from_db는 동기 호출 — 조회 시간이 짧아 실용적으로 문제 없음)
async def _get_current_ci() -> float | None:
ci = get_latest_ci_from_db()
if ci is not None:
return ci
try:
return get_optimizer().get_current_ci()
except Exception:
return None
같은 원칙을 평가 스크립트(run_eval.py)에도 적용했다. CIASC 임계값을 계산하려고 평가 루프 안에서 매 쿼리마다 apply_mode("ciasc")를 다시 호출하고 있었는데, 이건 루프 시작 전에 한 번만 계산하면 되는 값이었다. optimizer에 5분 TTL 캐시가 있어서 25번 모두 실제 HTTP 요청이 나가는 건 아니지만, 불필요한 임계값 재계산(및 잠재적 API 호출)이 25번 발생하는 구조였다. 해결책은 단순했다 — 루프 밖에서 한 번만 계산하고 끝.
병목 2: EmissionsTracker 초기화 오버헤드
DB 호출을 없앤 다음에도 여전히 첫 요청이 5초 넘게 걸렸다. timings를 다시 뜯어보니 이번엔 carbon 모니터링 자체가 느렸다.
문제는 EmissionsTracker 인스턴스를 매 요청마다 새로 만들고 있었다는 거다. CodeCarbon 라이브러리의 EmissionsTracker는 초기화할 때 GPU/CPU 하드웨어 탐지를 수행한다. 이게 생각보다 비싸서 매번 ~5초씩 잡아먹고 있었다.
# 변경 전: track() 호출할 때마다 새 인스턴스 생성
# @contextmanager
def track(self, stage_name, ...):
tracker = self._build_tracker() # 매번 하드웨어 탐지
tracker.start()
...
# 변경 후: __init__에서 한 번만 만들고 재사용
def __init__(self, ...):
...
self._tracker = self._build_tracker() # 딱 한 번
# @contextmanager
def track(self, stage_name, ...):
tracker = self._tracker # 캐시된 인스턴스 사용
tracker.start()
...
allow_multiple_runs=True 옵션이 이미 설정되어 있어서 같은 인스턴스를 반복 사용하는 건 이미 고려된 구조였다. 그냥 초기화를 밖으로 빼주기만 하면 됐다.
결과: 캐시 히트 케이스 기준 5100ms → 610ms, 8.4배 개선.


남은 과제
이제 어느정도의 채팅 성능이 보장된 상태이다. 남은 것은 채팅할 때 외부 LLM API를 호출하거나 더 빠르고 좋은 환경에서 서버를 실행하는 것이다. 그리고 캐싱 전략과 동적으로 threshold 값을 바꿀 수 있게 하는 핵심 로직을 구현하면 된다.
- API 바꾸기
- 캐시되지 않은 답변은 캐시로 저장하기
- threshold가 Carbon API 값에 따라 동적으로 변화하여 사용자 경험과 탄소 중립 목표 실현하기
이 정도로 남은 과제를 요약할 수 있겠다.
정리하면서
돌아보면 두 문제 다 같은 패턴이다. 매 요청마다 안 해도 되는 것들을 매 요청마다 새로 하고 있었다. 외부 API 호출, 하드웨어 탐지 — 둘 다 비싼 작업이고, 둘 다 한 번만 해서 캐시할 수 있었다.
사실 병목을 처음 파악했을 때는 Carbon API 쪽만 의심했다. 그게 해결되고 나서도 여전히 느리길래 timings를 좀 더 꼼꼼히 들여다봤더니 두 번째 문제가 나왔다. 측정 코드가 없었으면 진단이 훨씬 오래 걸렸을 거다.
run_eval.py의 재호출 문제는 성능보다 정확성 문제이기도 했다. CI 값이 평가 도중에 바뀌면 같은 배치 안에서도 임계값이 달라지는 상황이 생길 수 있어서 아무래도 한 번만 계산하는 게 맞다.
앞으로 인프라 쪽에서 비슷한 패턴이 또 보이면 같은 방식으로 접근할 것 같다. 측정 먼저, 가정 나중에.
'탄소중립' 카테고리의 다른 글
| [탄소중립 아카데미 6기] API 구현 및 연동 (0) | 2026.05.25 |
|---|---|
| [탄소중립 아카데미 6기] 벡터 임베딩과 벡터 검색 (0) | 2026.04.27 |
| [탄소중립 아카데미 6기] 자동 크롤러 구현하기 (1) | 2026.04.13 |
| [탄소중립 아카데미 6기] RAG란 무엇인가? (0) | 2026.04.06 |
| [탄소중립 아키데미 6기] 1주차 - OT (0) | 2026.03.27 |