RAG 시스템 부하 테스트와 모니터링
지난 글에서 권한 기반 접근 제어를 다루면서, 다음으로 RAG 시스템 전체의 부하 테스트와 모니터링을 예고했습니다. 지금까지 검색·생성·캐싱·비동기 처리를 하나씩 붙여왔는데, 실제 트래픽 상황에서 이 조각들이 함께 잘 버티는지는 별도로 검증해야 합니다. 이번 글에서는 RAG 챗봇에 특화된 부하 테스트와 모니터링 포인트를 정리합니다.
1. 일반 API 부하 테스트와 무엇이 다른가
일반 REST API는 보통 응답 시간이 짧고 일정한 편이라 동시 사용자 수를 늘려가며 처리량(TPS)을 측정하는 방식이 잘 맞습니다. 하지만 RAG 챗봇은 요청마다 응답 시간 편차가 크고, 외부 LLM API의 처리량 자체가 병목이 될 수 있습니다.
일반 API : 요청당 50~200ms, 편차 작음
RAG 챗봇 API : 요청당 800~4000ms, 편차 큼 (캐시 히트 여부, 문서 길이, LLM 혼잡도에 따라)
그래서 RAG는 단순히 "동시 사용자 몇 명까지 버티는가"보다, "캐시 히트율이 낮은 최악의 상황에서도 응답 시간이 허용 범위 안에 있는가"를 함께 봐야 합니다.
2. JMeter로 RAG API 부하 테스트 시나리오 구성하기
이전 성능 최적화 시리즈에서 다룬 JMeter를 RAG 챗봇에도 그대로 활용할 수 있습니다. 다만 질문 세트를 구성할 때 캐시 히트/미스 비율을 의도적으로 섞어야 실제 운영 상황과 비슷한 결과를 얻을 수 있습니다.
Thread Group
- 동시 사용자 수: 50
- Ramp-up 시간: 30초
- 반복 횟수: 10
HTTP Request Sampler
- POST /api/chat
- Body: ${question} (CSV Data Set Config에서 질문 목록 순환)
questions.csv
"재택근무는 몇 회까지 가능해?" ← 반복 질문 (캐시 히트 유도)
"연차는 며칠 쓸 수 있어?" ← 반복 질문 (캐시 히트 유도)
"2024년 3분기 출장 정산 규정이 뭐야?" ← 매번 다른 질문 (캐시 미스 유도)
CSV 안에 자주 반복되는 질문과, 매번 새로운 질문을 의도적으로 섞어두면 캐시 히트율이 실제 서비스와 비슷한 비율로 재현됩니다.
3. 확인해야 할 핵심 지표
일반 부하 테스트에서 보는 응답 시간, 에러율 외에 RAG 특유의 지표를 추가로 봐야 합니다.
| 지표 | 확인 목적 |
|---|---|
| 캐시 히트율 | Redis 캐싱이 트래픽 증가 시에도 예상대로 동작하는지 |
| LLM API 호출 대기 시간 | 외부 API 자체가 병목인지, 우리 서버 처리가 병목인지 구분 |
| LLM API 에러/Rate Limit 발생 횟수 | 동시 요청이 몰릴 때 외부 API 제한에 걸리는지 |
| DB 커넥션 풀 대기 시간 | 벡터 검색 쿼리가 몰릴 때 HikariCP 풀이 부족한지 |
| 스레드 풀 큐 대기 | 비동기 처리(@Async)로 넘긴 작업이 밀리고 있는지 |
4. LLM API가 병목인지 우리 서버가 병목인지 구분하기
부하 테스트 결과 응답 시간이 늘어났을 때, 원인이 우리 시스템인지 외부 LLM API인지 구분하지 못하면 엉뚱한 곳을 최적화하게 됩니다. 지난 글에서 다룬 APM으로 이 구간을 명확히 나눠서 봐야 합니다.
POST /api/chat 총 3200ms
├─ 벡터 검색 (DB) 40ms
├─ Reranker 호출 180ms
├─ LLM 호출 (OpenAI) 2900ms <- 대부분의 시간
└─ 응답 직렬화 5ms
이 경우처럼 LLM 호출이 전체 시간의 대부분을 차지한다면, 우리 서버 코드를 아무리 최적화해도 체감 개선은 크지 않습니다. 이런 상황에서는 스트리밍 응답(이전 글에서 다룬 SSE 방식)으로 사용자 체감 대기 시간을 줄이거나, 캐싱 적중률을 높이는 방향이 더 효과적입니다.
5. Rate Limit 대응 - 재시도와 서킷 브레이커
동시 요청이 몰리면 LLM API 제공사의 Rate Limit(분당 호출 제한)에 걸릴 수 있습니다. 이 경우를 대비해 재시도와 서킷 브레이커를 함께 적용합니다.
@Service
@RequiredArgsConstructor
public class LlmCallService {
private final ChatClient chatClient;
@Retryable(
retryFor = {RateLimitException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 500, multiplier = 2)
)
@CircuitBreaker(name = "llmService", fallbackMethod = "fallbackAnswer")
public String call(String prompt) {
return chatClient.prompt().user(prompt).call().content();
}
public String fallbackAnswer(String prompt, Throwable t) {
return "현재 요청이 몰려 답변 생성이 지연되고 있습니다. 잠시 후 다시 시도해주세요.";
}
}
Rate Limit에 걸리면 지수 백오프로 짧게 재시도하고, 그래도 계속 실패하면 서킷 브레이커가 열려서 즉시 안내 문구를 반환합니다. 이렇게 하면 LLM API 장애가 전체 서비스 장애로 번지는 것을 막을 수 있습니다.
6. 모니터링 대시보드 구성
부하 테스트로 확인한 지표들은 운영 중에도 지속적으로 관찰해야 하므로, 지난 글에서 다룬 APM과 별도로 RAG 특화 지표를 대시보드에 추가합니다.
# 모니터링 지표 예시 (Prometheus + Grafana 기준)
metrics:
- name: rag_cache_hit_ratio
type: gauge
description: "최근 5분간 답변 캐시 히트율"
- name: rag_llm_call_duration_seconds
type: histogram
description: "LLM API 호출 소요 시간 분포"
- name: rag_llm_rate_limit_count
type: counter
description: "Rate Limit 발생 횟수"
- name: rag_search_relevance_score
type: gauge
description: "검색 결과 평균 유사도 점수 (품질 저하 조기 감지용)"
특히 rag_search_relevance_score처럼 검색 품질 관련 지표를 함께 모니터링해두면, 지난 글에서 다룬 RAGAS 평가를 매번 수동으로 돌리지 않아도 검색 품질이 서서히 저하되는 징후를 운영 중에 조기 발견할 수 있습니다.
7. 부하 테스트 결과 예시와 해석
[테스트 조건] 동시 사용자 50명, 질문 세트에 캐시 히트 질문 70% 포함
캐시 히트 요청 : 평균 응답시간 45ms, TPS 180
캐시 미스 요청 : 평균 응답시간 2800ms, TPS 12
전체 에러율 : 0.3% (LLM Rate Limit 재시도 후 성공)
DB 커넥션 풀 : 최대 사용률 62%, 대기 없음
이 결과라면 DB 계층은 여유가 있고, 병목은 명확히 LLM 호출 구간에 있다는 걸 알 수 있습니다. 그렇다면 다음 우선순위는 서버 코드 튜닝이 아니라 캐시 히트율을 더 끌어올리거나, LLM 호출 자체를 병렬화하는 방향으로 잡아야 합니다.
8. 정리
- RAG 부하 테스트는 캐시 히트/미스 비율을 의도적으로 섞은 질문 세트로 진행해야 실제 운영 상황과 가까운 결과를 얻을 수 있습니다.
- 응답 시간 지연의 원인이 우리 서버인지 외부 LLM API인지 APM으로 구분해야, 엉뚱한 곳을 최적화하는 실수를 피할 수 있습니다.
- Rate Limit과 LLM 장애에 대비한 재시도·서킷 브레이커, 그리고 검색 품질 지표까지 포함한 모니터링 대시보드를 함께 갖춰야 운영 중 문제를 조기에 발견할 수 있습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 21 - 멀티모달 RAG (0) | 2026.07.20 |
|---|---|
| STEP 20 - GraphRAG (0) | 2026.07.15 |
| STEP 18 - 권한 기반 RAG 접근 제어 (0) | 2026.07.14 |
| STEP 17 - 대화형 RAG 챗봇 구현하기 (0) | 2026.07.14 |
| STEP 16 - Hybrid Search (0) | 2026.07.13 |