
운영을 하거나 배포 점검을 하다보면 프로세스를 종료해야하는 경우가 있는데,
이때 실행 중인 요청을 강제로 끊어버리면 데이터의 유실이 발생 될 수 있고, 유저의 사용 경험을 안좋게 만들 수 있습니다.
이번 포스팅에서는 "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 |