본문 바로가기
Backend2026년 7월 29일6분 읽기

타임아웃 예산과 데드라인 전파 — 유령 작업을 없애는 법

YS
김영삼
조회 4
타임아웃 예산과 데드라인 전파 — 유령 작업을 없애는 법

타임아웃 예산(timeout budget)과 데드라인 전파(deadline propagation)는 하나의 요청이 여러 서비스를 거칠 때, "이 요청은 언제까지 끝나야 한다"는 절대 시각을 정하고 그것을 하위 호출로 계속 넘겨주는 기법입니다. 각 단계가 제멋대로 타임아웃을 잡는 대신, 전체가 하나의 마감 시각을 공유하는 것이죠.

이걸 모르면 이상한 버그를 만납니다. 사용자에게는 이미 3초 만에 504를 돌려줬는데, 백엔드 깊은 곳에서는 그 죽은 요청을 위해 여전히 30초짜리 쿼리를 돌리고 있는 상황. 아무도 안 기다리는 일을 위해 자원을 태우는, 유령 작업 말입니다.

타임아웃을 각자 정하면 생기는 문제

서비스 체인이 A → B → C라고 합시다. 순진하게 만들면 각자 30초씩 타임아웃을 잡습니다. 문제는 이 값들이 서로를 모른다는 겁니다.

  • A가 20초를 기다리다 포기했는데, B는 그것도 모르고 C를 25초째 기다립니다. B와 C의 작업은 전부 낭비입니다.
  • 반대로 A의 타임아웃이 C보다 짧으면, C는 정상 응답을 만들어 보내도 이미 A가 끊어 폐기됩니다.

핵심은 "남은 시간"은 요청이 흘러갈수록 줄어든다는 사실입니다. A에서 5초를 이미 썼다면 B가 쓸 수 있는 시간은 30초가 아니라 25초여야 맞습니다.

타임아웃(상대) vs 데드라인(절대)

여기서 개념을 하나 나눠야 합니다. 타임아웃은 "지금부터 N초"라는 상대적 길이이고, 데드라인은 "오후 3시 0분 5초까지"라는 절대적 시각입니다. 데드라인 전파의 묘미는 절대 시각을 넘기는 데 있습니다.

방식전달하는 값문제
고정 타임아웃각 단계 "30초"합이 무한정 커짐
남은 시간 전달"이제 12초 남음"네트워크 지연만큼 오차
데드라인 전달"15:00:05까지"서버 간 시계 동기화 필요

Go에서의 context 전파

Go의 context.Context는 이 개념을 언어 표준으로 끌어올린 좋은 예입니다. 데드라인을 컨텍스트에 담아 모든 하위 호출에 넘기고, 각 함수는 남은 시간을 스스로 확인합니다.

func handler(w http.ResponseWriter, r *http.Request) {
    // 이 요청의 전체 데드라인: 지금부터 2초
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()
    // 하위 호출에 같은 ctx를 넘기면 데드라인이 그대로 전파된다
    user, err := fetchUser(ctx, id)   // ctx가 만료되면 여기서 즉시 취소
    if err != nil { ... }
    // DB 호출도 마찬가지 — 남은 시간만큼만 기다린다
    orders, err := db.QueryContext(ctx, "SELECT ...")
}

포인트는 ctx를 그냥 함수 인자로 계속 넘기기만 하면 된다는 겁니다. 상위에서 취소되면 그 아래 모든 호출이 연쇄적으로 취소됩니다. gRPC는 여기서 한 발 더 나아가, 데드라인을 네트워크 너머 다른 서비스로도 자동 전파합니다. 클라이언트가 데드라인을 걸면 서버 쪽 ctx.Deadline()에 그대로 반영돼요.

실무에서 지키는 규칙

내가 팀에 강제하는 규칙 몇 가지.

  • 바깥일수록 길고 안일수록 짧게 — 전체 예산에서 각 단계가 조금씩 떼어 씁니다. 안쪽 타임아웃이 바깥보다 길면 절대 안 됩니다.
  • 타임아웃 없는 네트워크 호출은 버그다 — 기본 무한 대기인 클라이언트가 의외로 많습니다. HTTP 클라이언트, DB 드라이버, 커넥션 풀 획득까지 전부 타임아웃을 명시하세요.
  • 취소를 실제로 처리하라 — 데드라인이 지났으면 하던 쿼리를 정말로 멈춰야 합니다. 신호만 받고 계속 돌면 유령 작업은 그대로입니다.
타임아웃 값을 "감으로 30초" 잡지 마세요. p99 지연 시간을 측정하고, 거기에 여유를 조금 얹어 정하는 게 맞습니다. 근거 없는 타임아웃은 근거 없는 장애로 돌아옵니다.

자주 묻는 질문

데드라인 전파에 서버 시계 동기화가 꼭 필요한가요?

절대 시각(데드라인)을 그대로 넘기면 서버 간 시계가 어긋난 만큼 오차가 납니다. 그래서 실무에선 종종 "남은 시간(예: 1200ms)"을 전달하고 받는 쪽이 자기 시계 기준으로 다시 데드라인을 계산합니다. 이러면 시계 동기화 없이도 네트워크 지연만큼의 작은 오차로 끝납니다. gRPC의 타임아웃도 이 방식(상대 시간 전달)입니다.

재시도가 있으면 예산을 어떻게 나누나요?

재시도까지 포함해 전체 예산 안에 들어와야 합니다. 예산이 2초인데 한 번에 1.5초가 걸리면 재시도할 시간이 없죠. 그래서 재시도를 염두에 둔다면 개별 시도의 타임아웃을 전체 예산보다 넉넉히 짧게 잡아야 합니다.

타임아웃을 무한대로 두면 안 되는 이유가 뭔가요?

느린 응답 하나가 스레드나 커넥션을 붙잡고 놓지 않으면, 그런 요청이 쌓여 풀이 고갈되고 결국 멀쩡한 요청까지 막힙니다. 타임아웃은 "이 자원을 언제 회수할지"를 정하는 안전장치입니다. 무한 대기는 자원 누수의 지름길입니다.

클라이언트가 이미 끊었는지 서버가 알 수 있나요?

알 수 있고, 알아야 합니다. Go라면 ctx.Done()이 닫히고, HTTP 서버는 커넥션 종료를 감지합니다. 이 신호를 확인해 이미 버려진 요청의 무거운 작업을 중단하면 자원을 크게 아낄 수 있습니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록