Updated:

15 minute read

개요

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 사용, 타임아웃 설정, 로깅, 여러 시그널 처리, 상태 저장, 테스트, 메트릭, 문서화