[Go] 시그널
개요
Go의 os/signal 패키지는 운영체제의 시그널을 처리하여 프로그램의 우아한 종료(graceful shutdown)를 구현합니다.
주요 특징:
- 시그널 감지: SIGINT, SIGTERM 등 OS 시그널 수신
- 채널 기반: Go 채널로 시그널 전달
- Graceful Shutdown: 리소스 정리 후 종료
- Context 통합: context와 결합한 시그널 처리
- 다중 시그널: 여러 시그널 동시 처리
- 플랫폼 독립: 크로스 플랫폼 시그널 처리
- 실시간 처리: 비동기 시그널 핸들링
기본 개념
1. 기본 시그널 처리
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// 시그널 수신 채널
sigChan := make(chan os.Signal, 1)
// SIGINT, SIGTERM 감지
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("서버 시작...")
// 시그널 대기
sig := <-sigChan
fmt.Printf("\n받은 시그널: %v\n", sig)
fmt.Println("프로그램 종료")
}
2. 주요 시그널
import "syscall"
func main() {
sigChan := make(chan os.Signal, 1)
// 다양한 시그널
signal.Notify(sigChan,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // kill 명령
syscall.SIGHUP, // 터미널 종료
syscall.SIGQUIT, // Ctrl+\
)
sig := <-sigChan
switch sig {
case syscall.SIGINT:
fmt.Println("인터럽트 (Ctrl+C)")
case syscall.SIGTERM:
fmt.Println("종료 요청")
case syscall.SIGHUP:
fmt.Println("재시작 요청")
case syscall.SIGQUIT:
fmt.Println("강제 종료")
}
}
3. 시그널 중지
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
fmt.Println("시그널 감지 시작")
// 잠시 대기
time.Sleep(5 * time.Second)
// 시그널 감지 중지
signal.Stop(sigChan)
close(sigChan)
fmt.Println("시그널 감지 중지")
// 이후 Ctrl+C 해도 처리 안 됨
time.Sleep(5 * time.Second)
}
4. Context 기반 시그널
import "context"
func main() {
// Context와 시그널 통합
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer stop()
fmt.Println("서버 시작...")
// Context가 취소될 때까지 대기
<-ctx.Done()
fmt.Println("\n종료 시그널 수신")
fmt.Println("정리 작업 수행...")
time.Sleep(2 * time.Second)
fmt.Println("프로그램 종료")
}
Graceful Shutdown
1. HTTP 서버
import (
"net/http"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}),
}
// 시그널 처리
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// 서버 시작
go func() {
fmt.Println("서버 시작: http://localhost:8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("서버 에러: %v\n", err)
}
}()
// 시그널 대기
<-sigChan
fmt.Println("\n종료 시그널 수신, 서버 종료 중...")
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("서버 종료 에러: %v\n", err)
}
fmt.Println("서버 종료 완료")
}
2. 워커 풀
import "sync"
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("워커 %d: 작업 %d 처리\n", id, job)
time.Sleep(1 * time.Second)
}
fmt.Printf("워커 %d 종료\n", id)
}
func main() {
const numWorkers = 3
jobs := make(chan int, 10)
var wg sync.WaitGroup
// 워커 시작
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, &wg)
}
// 작업 추가
go func() {
for i := 1; i <= 20; i++ {
jobs <- i
}
}()
// 시그널 대기
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("\n종료 시그널 수신")
// 새 작업 중지
close(jobs)
// 모든 워커 종료 대기
fmt.Println("모든 워커 종료 대기 중...")
wg.Wait()
fmt.Println("프로그램 종료")
}
3. 타임아웃과 강제 종료
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
done := make(chan bool)
// 긴 작업 시뮬레이션
go func() {
fmt.Println("작업 시작...")
time.Sleep(30 * time.Second)
fmt.Println("작업 완료")
done <- true
}()
select {
case <-sigChan:
fmt.Println("\n종료 시그널 수신, 정리 중...")
// 5초 타임아웃
timeout := time.After(5 * time.Second)
select {
case <-done:
fmt.Println("정상 종료")
case <-timeout:
fmt.Println("타임아웃, 강제 종료")
}
case <-done:
fmt.Println("작업 정상 완료")
}
}
실전 예제
1. HTTP 서버 Graceful Shutdown
import (
"context"
"net/http"
"sync"
"time"
)
type Server struct {
httpServer *http.Server
wg sync.WaitGroup
}
func NewServer() *Server {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Slow response")
})
return &Server{
httpServer: &http.Server{
Addr: ":8080",
Handler: mux,
},
}
}
func (s *Server) Start() error {
fmt.Println("서버 시작: http://localhost:8080")
return s.httpServer.ListenAndServe()
}
func (s *Server) Shutdown(ctx context.Context) error {
fmt.Println("서버 종료 시작...")
if err := s.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("서버 종료 실패: %w", err)
}
fmt.Println("HTTP 서버 종료 완료")
return nil
}
func main() {
server := NewServer()
// 서버 시작
go func() {
if err := server.Start(); err != http.ErrServerClosed {
fmt.Printf("서버 에러: %v\n", err)
os.Exit(1)
}
}()
// 시그널 대기
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer stop()
<-ctx.Done()
fmt.Println("\n종료 시그널 수신")
// 30초 타임아웃으로 종료
shutdownCtx, cancel := context.WithTimeout(
context.Background(),
30*time.Second,
)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
fmt.Printf("에러: %v\n", err)
os.Exit(1)
}
fmt.Println("프로그램 정상 종료")
}
2. 데이터베이스 연결 정리
import (
"database/sql"
_ "github.com/lib/pq"
)
type App struct {
db *sql.DB
}
func NewApp() (*App, error) {
db, err := sql.Open("postgres", "postgresql://...")
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return &App{db: db}, nil
}
func (a *App) Close() error {
fmt.Println("데이터베이스 연결 종료...")
// 진행 중인 쿼리 완료 대기
if err := a.db.Close(); err != nil {
return fmt.Errorf("DB 종료 실패: %w", err)
}
fmt.Println("데이터베이스 연결 종료 완료")
return nil
}
func (a *App) Run(ctx context.Context) error {
// 주기적 작업
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
// DB 작업
var count int
err := a.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
return err
}
fmt.Printf("사용자 수: %d\n", count)
}
}
}
func main() {
app, err := NewApp()
if err != nil {
fmt.Printf("앱 초기화 실패: %v\n", err)
os.Exit(1)
}
defer app.Close()
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer stop()
fmt.Println("앱 시작...")
if err := app.Run(ctx); err != nil && err != context.Canceled {
fmt.Printf("앱 에러: %v\n", err)
os.Exit(1)
}
fmt.Println("앱 정상 종료")
}
3. 설정 리로드 (SIGHUP)
import (
"encoding/json"
"io/ioutil"
"sync"
)
type Config struct {
Port int `json:"port"`
LogLevel string `json:"log_level"`
mu sync.RWMutex
}
func (c *Config) Load(filename string) error {
c.mu.Lock()
defer c.mu.Unlock()
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return json.Unmarshal(data, c)
}
func (c *Config) GetPort() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.Port
}
func (c *Config) GetLogLevel() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.LogLevel
}
func main() {
configFile := "config.json"
config := &Config{}
// 초기 설정 로드
if err := config.Load(configFile); err != nil {
fmt.Printf("설정 로드 실패: %v\n", err)
os.Exit(1)
}
fmt.Printf("초기 설정: Port=%d, LogLevel=%s\n",
config.GetPort(), config.GetLogLevel())
// 시그널 처리
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
for {
sig := <-sigChan
switch sig {
case syscall.SIGHUP:
fmt.Println("\n설정 리로드 요청 (SIGHUP)")
if err := config.Load(configFile); err != nil {
fmt.Printf("설정 리로드 실패: %v\n", err)
} else {
fmt.Printf("설정 리로드 완료: Port=%d, LogLevel=%s\n",
config.GetPort(), config.GetLogLevel())
}
case syscall.SIGINT, syscall.SIGTERM:
fmt.Println("\n종료 시그널 수신")
fmt.Println("프로그램 종료")
return
}
}
}
4. 멀티 고루틴 종료 관리
type Service struct {
name string
ctx context.Context
wg *sync.WaitGroup
}
func NewService(name string, ctx context.Context, wg *sync.WaitGroup) *Service {
return &Service{
name: name,
ctx: ctx,
wg: wg,
}
}
func (s *Service) Run() {
defer s.wg.Done()
fmt.Printf("[%s] 시작\n", s.name)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-s.ctx.Done():
fmt.Printf("[%s] 종료\n", s.name)
return
case <-ticker.C:
fmt.Printf("[%s] 작업 중...\n", s.name)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
// 여러 서비스 시작
services := []string{"API", "Worker", "Monitor"}
for _, name := range services {
wg.Add(1)
service := NewService(name, ctx, &wg)
go service.Run()
}
// 시그널 대기
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("\n종료 시그널 수신")
// Context 취소로 모든 서비스 종료 요청
cancel()
// 모든 서비스 종료 대기
fmt.Println("모든 서비스 종료 대기 중...")
wg.Wait()
fmt.Println("프로그램 종료")
}
5. 클린업 체인
type CleanupFunc func() error
type Cleaner struct {
funcs []CleanupFunc
mu sync.Mutex
}
func NewCleaner() *Cleaner {
return &Cleaner{
funcs: make([]CleanupFunc, 0),
}
}
func (c *Cleaner) Add(f CleanupFunc) {
c.mu.Lock()
defer c.mu.Unlock()
c.funcs = append(c.funcs, f)
}
func (c *Cleaner) Cleanup() error {
c.mu.Lock()
defer c.mu.Unlock()
var errs []error
// 역순으로 정리 (LIFO)
for i := len(c.funcs) - 1; i >= 0; i-- {
if err := c.funcs[i](); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("정리 중 에러 발생: %v", errs)
}
return nil
}
func main() {
cleaner := NewCleaner()
// 데이터베이스 연결
fmt.Println("데이터베이스 연결...")
cleaner.Add(func() error {
fmt.Println("데이터베이스 연결 종료")
return nil
})
// 캐시 연결
fmt.Println("캐시 연결...")
cleaner.Add(func() error {
fmt.Println("캐시 연결 종료")
return nil
})
// 파일 열기
fmt.Println("로그 파일 열기...")
cleaner.Add(func() error {
fmt.Println("로그 파일 닫기")
return nil
})
// 시그널 대기
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("\n서비스 실행 중... (Ctrl+C로 종료)")
<-sigChan
fmt.Println("\n종료 시그널 수신")
fmt.Println("정리 작업 시작...")
if err := cleaner.Cleanup(); err != nil {
fmt.Printf("정리 에러: %v\n", err)
os.Exit(1)
}
fmt.Println("프로그램 정상 종료")
}
6. 프로그레스 저장 및 복구
import (
"encoding/json"
"os"
)
type Progress struct {
ProcessedItems int `json:"processed_items"`
TotalItems int `json:"total_items"`
LastUpdated time.Time `json:"last_updated"`
}
func (p *Progress) Save(filename string) error {
data, err := json.MarshalIndent(p, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(filename, data, 0644)
}
func LoadProgress(filename string) (*Progress, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
if os.IsNotExist(err) {
return &Progress{}, nil
}
return nil, err
}
var p Progress
if err := json.Unmarshal(data, &p); err != nil {
return nil, err
}
return &p, nil
}
func main() {
progressFile := "progress.json"
// 진행 상태 로드
progress, err := LoadProgress(progressFile)
if err != nil {
fmt.Printf("진행 상태 로드 실패: %v\n", err)
os.Exit(1)
}
if progress.ProcessedItems > 0 {
fmt.Printf("이전 진행 상태 복구: %d/%d\n",
progress.ProcessedItems, progress.TotalItems)
}
progress.TotalItems = 100
// 시그널 처리
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer stop()
// 작업 수행
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
fmt.Println("\n종료 시그널 수신")
// 진행 상태 저장
progress.LastUpdated = time.Now()
if err := progress.Save(progressFile); err != nil {
fmt.Printf("진행 상태 저장 실패: %v\n", err)
os.Exit(1)
}
fmt.Printf("진행 상태 저장: %d/%d\n",
progress.ProcessedItems, progress.TotalItems)
return
case <-ticker.C:
if progress.ProcessedItems < progress.TotalItems {
progress.ProcessedItems++
fmt.Printf("진행: %d/%d\n",
progress.ProcessedItems, progress.TotalItems)
} else {
fmt.Println("모든 작업 완료!")
return
}
}
}
}
7. 로깅과 메트릭 플러시
import (
"log"
"sync"
)
type Logger struct {
buffer []string
mu sync.Mutex
file *os.File
}
func NewLogger(filename string) (*Logger, error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
return &Logger{
buffer: make([]string, 0),
file: file,
}, nil
}
func (l *Logger) Log(msg string) {
l.mu.Lock()
defer l.mu.Unlock()
timestamp := time.Now().Format("2006-01-02 15:04:05")
l.buffer = append(l.buffer, fmt.Sprintf("[%s] %s", timestamp, msg))
}
func (l *Logger) Flush() error {
l.mu.Lock()
defer l.mu.Unlock()
if len(l.buffer) == 0 {
return nil
}
fmt.Printf("로그 버퍼 플러시: %d개 항목\n", len(l.buffer))
for _, msg := range l.buffer {
if _, err := l.file.WriteString(msg + "\n"); err != nil {
return err
}
}
l.buffer = l.buffer[:0]
return l.file.Sync()
}
func (l *Logger) Close() error {
if err := l.Flush(); err != nil {
return err
}
return l.file.Close()
}
func main() {
logger, err := NewLogger("app.log")
if err != nil {
log.Fatal(err)
}
defer logger.Close()
// 주기적 플러시
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
logger.Flush()
}
}
}()
// 작업 수행
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
logger.Log("작업 수행 중")
}
}
}()
// 시그널 대기
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("\n종료 시그널 수신")
cancel()
fmt.Println("최종 플러시...")
if err := logger.Flush(); err != nil {
fmt.Printf("플러시 에러: %v\n", err)
}
fmt.Println("프로그램 종료")
}
8. 헬스체크와 종료
type HealthChecker struct {
healthy bool
mu sync.RWMutex
}
func NewHealthChecker() *HealthChecker {
return &HealthChecker{healthy: true}
}
func (h *HealthChecker) SetHealthy(healthy bool) {
h.mu.Lock()
defer h.mu.Unlock()
h.healthy = healthy
}
func (h *HealthChecker) IsHealthy() bool {
h.mu.RLock()
defer h.mu.RUnlock()
return h.healthy
}
func (h *HealthChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.IsHealthy() {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
} else {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, "Shutting down")
}
}
func main() {
health := NewHealthChecker()
// 헬스체크 엔드포인트
http.Handle("/health", health)
// 메인 핸들러
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !health.IsHealthy() {
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
fmt.Fprintf(w, "Hello, World!")
})
server := &http.Server{Addr: ":8080"}
// 서버 시작
go func() {
fmt.Println("서버 시작: http://localhost:8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("서버 에러: %v\n", err)
}
}()
// 시그널 대기
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("\n종료 시그널 수신")
// 헬스체크 실패로 변경 (로드밸런서가 제거)
health.SetHealthy(false)
fmt.Println("헬스체크 상태: 종료 중")
// 로드밸런서가 제거할 시간 대기
time.Sleep(5 * time.Second)
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fmt.Println("서버 종료 시작...")
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("서버 종료 에러: %v\n", err)
}
fmt.Println("프로그램 정상 종료")
}
일반적인 실수
1. 버퍼 없는 채널
// ❌ 나쁜 예 (시그널 손실 가능)
func main() {
sigChan := make(chan os.Signal) // 버퍼 없음
signal.Notify(sigChan, syscall.SIGINT)
time.Sleep(5 * time.Second) // 시그널이 와도 못 받을 수 있음
<-sigChan
}
// ✅ 좋은 예 (버퍼 사용)
func main() {
sigChan := make(chan os.Signal, 1) // 버퍼 1
signal.Notify(sigChan, syscall.SIGINT)
time.Sleep(5 * time.Second)
<-sigChan // 버퍼에 저장된 시그널 받음
}
2. signal.Stop 누락
// ❌ 나쁜 예 (리소스 누수)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
// signal.Stop 호출 안 함
close(sigChan)
}
// ✅ 좋은 예 (정리)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
// 시그널 감지 중지
signal.Stop(sigChan)
close(sigChan)
}
3. Graceful Shutdown 타임아웃 없음
// ❌ 나쁜 예 (무한 대기 가능)
func main() {
server := &http.Server{Addr: ":8080"}
go server.ListenAndServe()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
// 타임아웃 없이 종료
server.Shutdown(context.Background())
}
// ✅ 좋은 예 (타임아웃 설정)
func main() {
server := &http.Server{Addr: ":8080"}
go server.ListenAndServe()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
// 30초 타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(ctx)
}
4. 정리 작업 순서 무시
// ❌ 나쁜 예 (순서 고려 안 함)
func main() {
db := openDB()
cache := openCache()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
// 순서 없이 정리
db.Close()
cache.Close()
}
// ✅ 좋은 예 (역순 정리)
func main() {
db := openDB()
defer db.Close() // 마지막에 실행
cache := openCache()
defer cache.Close() // 먼저 실행
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
}
5. 고루틴 누수
// ❌ 나쁜 예 (고루틴 종료 안 됨)
func main() {
go func() {
for {
// 무한 루프
time.Sleep(1 * time.Second)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
// 고루틴이 계속 실행됨
}
// ✅ 좋은 예 (Context로 제어)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
return
default:
time.Sleep(1 * time.Second)
}
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
cancel() // 고루틴 종료
time.Sleep(100 * time.Millisecond) // 종료 대기
}
6. 에러 무시
// ❌ 나쁜 예 (에러 처리 안 함)
func main() {
server := &http.Server{Addr: ":8080"}
go server.ListenAndServe()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx) // 에러 무시
}
// ✅ 좋은 예 (에러 처리)
func main() {
server := &http.Server{Addr: ":8080"}
go server.ListenAndServe()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("서버 종료 에러: %v\n", err)
os.Exit(1)
}
}
7. 플랫폼별 시그널 무시
// ❌ 나쁜 예 (Windows에서 동작 안 함)
func main() {
sigChan := make(chan os.Signal, 1)
// Windows에서 지원 안 되는 시그널
signal.Notify(sigChan, syscall.SIGUSR1)
<-sigChan
}
// ✅ 좋은 예 (크로스 플랫폼)
func main() {
sigChan := make(chan os.Signal, 1)
// 모든 플랫폼에서 동작
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
}
베스트 프랙티스
1. NotifyContext 사용
// ✅ Context 기반 시그널 처리
func main() {
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer stop()
// Context를 전파
go worker(ctx)
<-ctx.Done()
fmt.Println("종료")
}
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
// 작업
}
}
}
2. 타임아웃 설정
// ✅ 강제 종료 방지
func gracefulShutdown(cleanup func() error) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
done := make(chan error, 1)
go func() {
done <- cleanup()
}()
select {
case err := <-done:
if err != nil {
fmt.Printf("정리 에러: %v\n", err)
}
case <-time.After(30 * time.Second):
fmt.Println("타임아웃, 강제 종료")
}
}
3. 로깅
// ✅ 시그널 로깅
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
for sig := range sigChan {
log.Printf("시그널 수신: %v (시각: %v)",
sig, time.Now().Format(time.RFC3339))
switch sig {
case syscall.SIGHUP:
log.Println("설정 리로드")
case syscall.SIGINT, syscall.SIGTERM:
log.Println("프로그램 종료")
return
}
}
}
4. 여러 시그널 처리
// ✅ 시그널별 다른 처리
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // kill
syscall.SIGHUP, // reload
)
for sig := range sigChan {
switch sig {
case syscall.SIGHUP:
handleReload()
case syscall.SIGINT, syscall.SIGTERM:
handleShutdown()
return
}
}
}
func handleReload() {
fmt.Println("설정 리로드")
}
func handleShutdown() {
fmt.Println("종료 처리")
}
5. 상태 저장
// ✅ 종료 전 상태 저장
type App struct {
state map[string]interface{}
}
func (a *App) SaveState(filename string) error {
data, err := json.Marshal(a.state)
if err != nil {
return err
}
return ioutil.WriteFile(filename, data, 0644)
}
func main() {
app := &App{state: make(map[string]interface{})}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
if err := app.SaveState("state.json"); err != nil {
log.Printf("상태 저장 실패: %v", err)
}
}
6. 테스트
// ✅ 시그널 핸들링 테스트
func TestGracefulShutdown(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan bool)
go func() {
<-ctx.Done()
// 정리 작업
done <- true
}()
// 종료 시뮬레이션
cancel()
select {
case <-done:
// 성공
case <-time.After(1 * time.Second):
t.Error("타임아웃")
}
}
7. 메트릭 수집
// ✅ 종료 메트릭
type Metrics struct {
shutdownDuration time.Duration
mu sync.Mutex
}
func (m *Metrics) RecordShutdown(d time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.shutdownDuration = d
}
func main() {
metrics := &Metrics{}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
<-sigChan
start := time.Now()
// 정리 작업
time.Sleep(2 * time.Second)
metrics.RecordShutdown(time.Since(start))
fmt.Printf("종료 시간: %v\n", time.Since(start))
}
8. 문서화
// ✅ 명확한 문서화
// GracefulShutdown handles application shutdown gracefully.
// It waits for the specified signals and calls cleanup with a timeout.
//
// Parameters:
// - cleanup: function to call during shutdown
// - timeout: maximum time to wait for cleanup
// - signals: OS signals to listen for
//
// Returns an error if cleanup fails or times out.
func GracefulShutdown(
cleanup func() error,
timeout time.Duration,
signals ...os.Signal,
) error {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, signals...)
defer signal.Stop(sigChan)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- cleanup()
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return fmt.Errorf("cleanup timeout after %v", timeout)
}
}
정리
- 기본: signal.Notify로 시그널 수신, 채널 기반 처리, signal.Stop으로 정리
- Context: NotifyContext로 시그널과 Context 통합
- Graceful: HTTP 서버, 워커 풀, DB 연결 등 우아한 종료
- 실전: HTTP 서버, DB 정리, 설정 리로드, 멀티 고루틴, 클린업 체인, 프로그레스 저장, 로깅 플러시, 헬스체크
- 실수: 버퍼 없는 채널, Stop 누락, 타임아웃 없음, 정리 순서 무시, 고루틴 누수, 에러 무시, 플랫폼별 시그널
- 베스트: NotifyContext 사용, 타임아웃 설정, 로깅, 여러 시그널 처리, 상태 저장, 테스트, 메트릭, 문서화