도입
백엔드에서 응답 지연을 다루는 순간부터 관점이 바뀝니다. “어떤 함수가 느리냐”보다, 어디에서 기다림이 쌓이는지를 찾아야 합니다. DB 쿼리가 느릴 수도 있지만, 스레드 풀 고갈, 커넥션 풀 고갈, 락 경합, 외부 API 지연, 디스크 I/O 대기 같은 “시스템 대기”가 원인인 경우가 많습니다.
이 글은 “응답이 느려졌을 때” 신입도 따라갈 수 있도록 진단 순서(트리아지) → 대표 패턴 → 재발 방지 흐름으로 정리합니다.
“Latency는 연산이 아니라 대기가 만든다.
대기는 보통 공유 자원과 의존성에서 시작된다.”
정의
평균(latency avg)은 “대부분의 요청이 빠를 때” 문제를 가립니다. 사용자가 느끼는 불만은 대체로 일부 요청이 비정상적으로 느려지는 꼬리 지연(Tail Latency)에서 시작합니다. 그래서 성능을 “좋게” 만든다는 건 평균을 10ms 줄이는 것보다, P99를 5초에서 500ms로 낮추는 것에 가깝습니다.
지표 관점
💡 TIP / “평균은 건강, P99는 면역력”
평균이 괜찮아도 P99가 흔들리면 운영은 불안정합니다. P99는 “예외 상황에서 시스템이 버티는 힘(면역력)”을 보여줍니다. 따라서 응답 지연 대응은 평균 최적화보다 꼬리 지연을 줄이는 구조 개선이 중심이 됩니다.
진단 순서(트리아지)
대표 패턴
패턴 1
요청 처리 스레드가 외부 호출(HTTP/DB)을 블로킹으로 기다리는 동안 점유되면, 동시에 들어오는 요청이 늘수록 스레드가 잠식됩니다. 스레드 풀은 결국 max에 도달하고, 이후 요청은 큐에서 대기하거나 타임아웃으로 떨어지며 P99가 급격히 증가합니다.
체크 포인트
- ✔ active thread가 max에 근접하는가?
- ✔ 요청 큐(queue)가 증가하는가?
- ✔ 특정 외부 호출/쿼리 시간이 튀는가(APM trace)?
- ✔ 타임아웃 설정이 과도하게 긴가?
👍 GOOD
- 다운스트림 타임아웃을 짧고 명확하게 설정(예: 1~2s)
- 서킷 브레이커/벌크헤드로 전염 차단
- 스레드풀/큐 사용률 모니터링 및 알람
👎 BAD
- 타임아웃 10~30초 + 공격적 재시도(지연 증폭)
- 요청마다 블로킹 작업을 무제한 수행
- 풀 고갈 시 “느려지다 멈춤” 형태로 장애 확산
패턴 2
CPU는 여유인데 응답이 느리다면, “연산”이 아니라 “대기”가 원인일 확률이 큽니다. 그 대표 지표가 iowait입니다. 흔한 케이스는 디스크에 로그를 과도하게 쓰거나, 동기 flush/fync가 잦아져 쓰기 대기가 늘어나는 상황입니다.
💡 TIP / 로그는 운영에서 “성능 비용”이 있다
요청/응답 전문 로깅, 대용량 JSON 문자열 생성은 디스크 I/O뿐 아니라 GC 압박까지 함께 유발합니다. 운영 환경에서는 비동기 로깅, 샘플링, 레벨 정책이 곧 성능 설계입니다.
실전 예시(운영 패턴 재구성)
상황: 특정 시간대에 P99가 6~8초로 급상승. 평균은 큰 변화 없음. CPU는 40% 수준으로 여유.
원인: 외부 API 지연이 300ms → 2~3초로 늘었고, 타임아웃이 10초로 길어서 요청 스레드가 오래 점유됨.
결과: 대기가 쌓이면서 스레드 풀이 잠식되고, 이후 요청은 큐에서 대기 → Tail Latency 폭발.
해결 전략
✅ 실무에서 바로 효과 나는 5가지
- ✔️ 다운스트림 타임아웃을 짧고 명확하게 설정(예: 1~2s)
- ✔️ 리트라이 정책을 제한(무한/공격적 재시도는 지연을 증폭)
- ✔️ 서킷 브레이커로 느린 의존성의 전염 차단
- ✔️ 벌크헤드(풀 분리)로 핵심 API 보호
- ✔️ 스레드풀/커넥션풀/큐 사용률 알람(예: 80%↑)
정리
신입일 때는 “어느 코드가 느리냐”에 집중하기 쉽지만, 운영에서는 “어디서 기다림이 쌓이냐”가 먼저입니다. 응답 지연을 제대로 다루려면 풀(스레드/커넥션), 타임아웃, 리트라이, 의존성 지연을 한 세트로 봐야 합니다. 이 관점을 갖추면, 같은 장애를 더 빨리 진단하고 재발까지 줄일 수 있습니다.