TECH 으로 돌아가기
TECH HACKER NEWS 어제 7분 읽기 55 READS

조급한 클라이언트가 서버를 무너뜨린다: 타임아웃과 재시도를 다시 생각하기

타임아웃 숫자, 그냥 감으로 박고 있진 않나요?

서버랑 통신하는 코드를 짜다 보면 꼭 만나는 게 타임아웃이랑 재시도예요. "응답이 3초 안에 안 오면 끊고, 실패하면 3번까지 다시 시도" 이런 식으로요. 그런데 이 숫자들, 솔직히 깊게 고민하고 정한 적 있으세요? 대부분은 "음… 한 3초면 되겠지" 하고 넘어가거든요. 이번 글은 바로 그 조급함(impatience) 이라는 걸 클라이언트와 서버 양쪽에서 제대로 들여다보는 이야기예요. 주인공 이름을 따서 '앨리스는 조급하다'는 제목이 붙었는데, 여기서 앨리스는 우리가 흔히 짜는 그 클라이언트라고 보면 돼요. 평소엔 잘 안 보이지만, 부하가 몰리는 순간 이 조급함이 시스템의 운명을 가르거든요.

앨리스가 포기하면, 서버는 헛수고를 합니다

핵심 통찰은 이거예요. 클라이언트가 타임아웃으로 요청을 포기했다고 해서, 서버가 그 요청 처리를 멈추는 건 아니라는 점이요. 이게 뭐냐면, 앨리스는 이미 "에이, 늦었네" 하고 등을 돌렸는데 서버는 그것도 모르고 묵묵히 그 요청을 끝까지 계산하고 있는 거예요. 다 끝내서 응답을 보내봤자 받을 사람이 없죠. 이걸 헛수고(wasted work) 라고 부르는데, 부하가 심할 때 이게 진짜 무섭습니다.

생각해 보면 서버가 바쁠수록 큐(처리 대기줄)가 길어지죠. 줄이 길어지면 앨리스가 기다리다 지쳐 포기하는 일도 잦아져요. 그럼 서버는 점점 더 "이미 아무도 안 기다리는 요청들"만 붙잡고 일하게 돼요. 들어오는 일은 많은데 정작 쓸모 있는 처리량, 즉 굿풋(goodput, 진짜로 응답이 전달된 처리량) 은 바닥을 치는 악순환이 만들어지는 거예요. CPU는 100%인데 성공하는 요청은 거의 없는, 그 답답한 상황이요.

재시도는 불난 집에 기름을 붓는 일

여기서 한 발 더 나가면 재시도가 문제를 키워요. 앨리스가 포기하고 곧장 같은 요청을 다시 던지면, 서버 입장에선 새 손님이 또 온 거예요. 원래 일 하나를 두 개, 세 개로 불려서 받는 셈이죠. 부하가 살짝 넘쳤을 뿐인데, 재시도가 부하를 더 키우고, 그 부하가 더 많은 타임아웃을 부르고, 또 더 많은 재시도를 부르고… 이렇게 한번 무너지면 원인이 사라져도 스스로 못 빠져나오는 상태를 메타스테이블 장애(metastable failure) 라고 해요. 트래픽을 평소 수준으로 되돌려도 서버가 계속 죽어 있는, 그 등골 서늘한 현상이죠. 그래서 재시도엔 꼭 지수 백오프(점점 간격 늘리기)랑 지터(무작위 흔들기), 그리고 전체 재시도 양을 제한하는 재시도 예산(retry budget) 이 필요해요.

큐잉 이론이 던지는 반전: 조급함이 약이 될 때도

재밌는 건, 클라이언트의 조급함이 꼭 나쁜 것만은 아니라는 거예요. 큐잉 이론에는 손님이 기다리다 줄을 떠나는 이탈(abandonment) 모델이 있는데, 손님이 적당히 포기해 주면 줄이 무한정 길어지는 걸 막아줘요. 일종의 자연스러운 부하 차단(load shedding)인 셈이죠. 다만 여기엔 조건이 있어요. 서버가 큐를 선입선출(FIFO) 로 처리하면 가장 오래 기다린, 즉 이미 포기했을 확률이 높은 요청부터 꺼내게 돼요. 헛수고 확률이 제일 높은 거죠. 반대로 과부하 상황에선 방금 들어온 신선한 요청부터 처리하는 후입선출(LIFO) 가 오히려 굿풋을 살리기도 해요. 방금 온 요청은 아직 누군가 기다리고 있을 가능성이 크니까요. "줄 선 순서대로"가 늘 정답은 아니라는 게 직관에 반하면서도 깊은 포인트예요.

그래서 데드라인을 흘려보내세요

실무적으로 가장 강력한 처방은 데드라인 전파(deadline propagation) 예요. 앨리스가 "난 1초까지만 기다릴게"라고 했다면, 그 1초라는 마감 시각을 요청 헤더에 담아서 서버로, 또 서버가 부르는 다른 서버로 계속 흘려보내는 거예요. 그럼 중간의 어떤 서비스가 일을 시작하려는 순간 "어차피 마감 지났네" 하고 바로 포기할 수 있거든요. 아무도 안 기다리는 일에 자원을 안 쓰는 거죠. 고정된 타임아웃 값을 각 계층마다 따로 박아두는 것보다 훨씬 똑똑한 방식이에요. gRPC 같은 프레임워크가 데드라인을 기본으로 밀어붙이는 이유가 여기에 있어요.

한국 개발자에게 주는 시사점

MSA(마이크로서비스)나 외부 API를 많이 엮는 환경이라면 이건 남 얘기가 아니에요. 타임아웃을 그냥 상수로 박아두기보다, 클라이언트의 마감 시각을 컨텍스트로 넘기는 구조를 한번 점검해 보세요. Go의 context.Context, 자바의 데드라인 기반 호출, 서비스 메시 설정 같은 게 다 이 이야기랑 직접 연결돼요. 또 재시도 로직을 넣을 때 "성공률은 올라가는데 장애 때 폭주하지 않나"를 꼭 같이 봐야 하고요. 부하 테스트할 때 평균 응답시간만 보지 말고, 과부하 구간에서 굿풋이 어떻게 무너지는지를 관찰하면 우리 시스템의 진짜 한계가 보여요.

마무리

결국 타임아웃과 재시도는 "얼마나 기다릴까"에 대한 추측이고, 그 추측이 시스템 전체의 안정성을 좌우해요. 한 줄 정리: 클라이언트가 이미 포기한 일에 서버가 매달리는 순간, 그 시스템은 조용히 무너지기 시작합니다. 여러분 서비스는 클라이언트의 마감 시각을 알고 있나요, 아니면 그냥 묵묵히 헛일을 하고 있나요?


🔗 출처: Hacker News

SOURCE · HACKER NEWS
원문 전체 보기 → https://brooker.co.za/blog/2026/06/19/waiting.html
SHARE
처리 중...