Go에서 Context를 이용한 Graceful Shutdown 구현

문제 상황

배포 시 진행 중이던 API 요청들이 중간에 끊기면서 클라이언트에서 에러가 발생했다. 특히 데이터 처리 시간이 긴 요청들이 문제였다.

해결 과정

Go의 contextos/signal 패키지를 사용해 graceful shutdown을 구현했다.

func main() {
    srv := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    log.Println("Server exited")
}

핵심 포인트

  • signal.Notify로 SIGTERM, SIGINT 신호를 받는다
  • srv.Shutdown()은 새 연결을 거부하고 기존 연결이 끝날 때까지 대기한다
  • Context timeout을 30초로 설정해 무한 대기를 방지했다
  • DB 커넥션 등 리소스 정리는 defer로 처리한다

결과

배포 시 진행 중인 요청이 완료된 후 서버가 종료되면서 에러율이 크게 감소했다. Kubernetes의 preStop hook과 함께 사용하니 더 안정적이었다.

타임아웃 값은 API의 최대 처리 시간을 고려해 조정이 필요하다. 우리 서비스는 30초면 충분했지만, 배치 작업이 있다면 더 길게 설정해야 할 것이다.