본문 바로가기

프로그래밍/golang

[Golang] 서버 종료 로직 구현 (Shutdown)

 

 

운영을 하거나 배포 점검을 하다보면 프로세스를 종료해야하는 경우가 있는데,
이때 실행 중인 요청을 강제로 끊어버리면 데이터의 유실이 발생 될 수 있고, 유저의 사용 경험을 안좋게 만들 수 있습니다.
이번 포스팅에서는 "Context" 와 "Siganl" 을 활용해서 안전하게 서버 종료하는 방법에 대해서 알아봅시다.

 

1. 예제 코드 : 기초 (OS 신호 감지)

단순 프로그램을 실행하고 종료하는 것이 아니라, 운영체제에서 보내는 종료 신호(SIGINT, SIGTERM)를 통해서
프로그램이 종료 됩니다.

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    fmt.Println("signal waiting...")
    
    <-ctx.Done()

    fmt.Println("Signal received, cleaning up resources...")
}

 

"signal.NotifiContext" 는 사용자가 "CTRL + C" 를 누르거나 터미널에서 종료 명령어를 입력하면
ctx 채널을 닫아주는 기능을 수행합니다.

 

2. 예제 코드 : Http

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    // HTTP 서버 설정
    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok"))
    })

    server := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
    }

    // 백그라운드 워커 실행 및 관리 (WaitGroup)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        ticker := time.NewTicker(500 * time.Millisecond)
        defer ticker.Stop()

        for {
            select {
            case <-ctx.Done(): // 신호 수신 시 워커 종료
                fmt.Println("worker: stopping:", ctx.Err())
                return
            case <-ticker.C:
                fmt.Println("worker: tick")
            }
        }
    }()

    // 서버 start
    go func() {
        fmt.Println("server: start :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("server error: %v\n", err)
        }
        fmt.Println("server: stopped")
    }()

    // 종료 대기
    <-ctx.Done()
    fmt.Println("main: signal received:", ctx.Err())

    // Shutdown
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Printf("server shutdown error: %v\n", err)
    }

    // 모든 워커가 종료될 때까지 대기
    wg.Wait()
    fmt.Println("system close")
}

 


프로그램을 단순히 종료하게 되면 그 즉시 모든 프로세스들이 종료가 되는데, 이 Shutdown을 사용하게 되면, 각 병렬적으로 실행되는
로직들이 안전하게 다 종료가 될 때까지 기다려주게 됩니다.
예를들면 사용자가 저장요청을 보냈다면, 저장 요청이 아직 진행 중인 상황에서 프로그램이 바로 종료가 되어버린다면 안될 것입니다.
사용자가 요청한 저장 로직은 온전히 다 완료가 된 이후에 프로그램이 종료되는 것이 훨씬 좋은 방법일 것 입니다.

위 코드에서 보면 종료 신호가 오더라도 반드시 context가 모두 닫힐 때까지 대기하고 있다가 종료가 되는 것을 확인 하실 수 있습니다.

 

3. 전체 코드

https://github.com/reochoi109/go-handbook/blob/main/shutdown/example/main.go

 

go-handbook/shutdown/example/main.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com

 

 

'프로그래밍 > golang' 카테고리의 다른 글

[Golang] net/url 기초  (0) 2026.03.24
[Golang] signalflight.Group  (0) 2026.03.21
[Golang] 고정 메모리 기반 Rate Limit  (0) 2026.03.21
[Golang] Rate Limit 패턴  (0) 2026.03.21
[Go] 고루틴 워커 풀(Worker Pool)  (0) 2026.03.21