빠른 흐름
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)종료 흐름
Shutdown은 새 연결을 막고 기존 요청을 기다린다
http.Server.Shutdown(ctx)는 서버 listener를 닫아 새 연결을 받지 않게 하고, 이미 처리 중인 요청이 끝나기를 기다립니다. timeout context가 끝나면 shutdown도 중단됩니다. 반면 Close()는 열린 연결을 즉시 닫으므로 graceful shutdown이 아닙니다.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown failed: %v", err)
}운영 서버에서는 종료 대기 시간을 무한대로 두지 않습니다. 배포 시스템의 termination grace period보다 짧은 timeout을 정해 프로세스가 예측 가능한 시간 안에 내려가게 합니다.
ListenAndServe의 http.ErrServerClosed는 정상 종료 신호다
Shutdown을 호출하면 ListenAndServe는 http.ErrServerClosed를 반환합니다. 이 값을 일반 오류처럼 log.Fatal로 처리하면 정상 종료가 실패처럼 기록됩니다. 서버 시작 goroutine에서는 이 오류를 별도로 제외해야 합니다.
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}서버 실행 오류와 종료 신호는 다른 경로로 다루는 편이 좋습니다. 포트 바인딩 실패는 시작 실패이고, ErrServerClosed는 shutdown 절차가 시작됐다는 신호입니다.
요청 handler는 r.Context()를 하위 작업에 넘긴다
graceful shutdown은 HTTP 서버 레벨에서만 끝나지 않습니다. handler가 DB 쿼리나 외부 API 호출을 오래 붙잡고 있으면 shutdown timeout까지 기다리게 됩니다. 요청 단위 작업에는 r.Context()를 넘겨 클라이언트 취소와 서버 종료 신호를 함께 반영해야 합니다.
func handler(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(), "SELECT * FROM jobs")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
}background worker는 request context가 아니라 애플리케이션 수명 context를 받아야 합니다. 요청이 끝났다는 이유로 전체 worker가 멈추면 안 됩니다.
운영 기준
| 상황 | 먼저 확인할 것 |
|---|---|
| 배포 중 연결 끊김 최소화 | Server.Shutdown |
| 즉시 강제 종료 | Server.Close |
| 종료 신호 수신 | signal.NotifyContext |
| 요청 하위 작업 취소 | r.Context() 전달 |
| shutdown 지연 | handler, DB, 외부 API timeout |
주의할 점
ListenAndServe 한 줄로 서버를 시작하면 종료 신호, timeout, 정상 종료 오류 처리를 놓치기 쉽습니다. 운영 서버는 http.Server를 직접 만들고, Shutdown timeout과 http.ErrServerClosed 처리를 명시해야 합니다.
graceful shutdown은 모든 작업을 끝까지 보장하는 장치가 아닙니다. timeout 안에 끝나지 않는 요청은 중단될 수 있습니다. 오래 걸리는 작업은 요청 handler 안에서 직접 끝내기보다 job queue나 background worker로 분리하고, 재시도 가능한 상태 저장을 함께 설계해야 합니다.
참고 링크
2 sources