ksundev 님의 블로그

Go 서버에서 Graceful Shutdown 구현하기 본문

[개발] Go

Go 서버에서 Graceful Shutdown 구현하기

ksundev 2025. 10. 19. 20:45

기본 구현 방법

먼저 코드부터 살펴보겠습니다:

func main() {
    app := fiber.New()

    // 라우트 설정
    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World!")
    })

    // 서버를 goroutine으로 실행
    go func() {
        if err := app.Listen(":3000"); err != nil {
            log.Panic(err)
        }
    }()

    // OS 신호 대기
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    <-c // 종료 신호 받을 때까지 대기

    log.Println("Gracefully shutting down...")
    app.Shutdown()
    log.Println("Server shut down successfully")
}

핵심은 서버를 goroutine 안에서 실행하는 것입니다.

왜 Goroutine이 필요한가?

핵심 이유

app.Listen()blocking 함수입니다. 서버가 종료될 때까지 다음 코드로 진행하지 않기 때문에:

  • ❌ goroutine 없이 실행 → signal을 받을 코드에 도달하지 못함
  • ✅ goroutine으로 실행 → 메인 스레드가 signal을 기다리다가 종료 신호를 받으면 graceful shutdown 수행 가능

전체 코드

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/gofiber/fiber/v2"
)

func main() {
    app := fiber.New()

    // 라우트 설정
    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World!")
    })

    // 서버를 goroutine으로 실행
    go func() {
        if err := app.Listen(":3000"); err != nil {
            log.Panic(err)
        }
    }()

    // OS 신호 대기
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)

    <-c // 종료 신호 받을 때까지 대기

    log.Println("Gracefully shutting down...")

    // Fiber 서버 종료
    if err := app.Shutdown(); err != nil {
        log.Fatal(err)
    }

    log.Println("Server shut down successfully")
}

동작 원리

Goroutine 없이 실행할 경우

func main() {
    app := fiber.New()

    app.Listen(":3000") // 여기서 계속 blocking

    // 아래 코드는 절대 실행되지 않음
    signal.Notify(...)
    <-c
}

서버가 계속 실행되면서 blocking되기 때문에 signal을 받을 코드에 도달할 수 없습니다.

Goroutine 사용할 경우

func main() {
    app := fiber.New()

    // 1. 이 goroutine은 백그라운드에서 계속 실행됨
    go func() {
        app.Listen(":3000") // 여기서 blocking되지만 goroutine 안이라 괜찮음
    }()

    // 2. main 함수는 여기로 바로 넘어옴

    // 3. 여기서 blocking - 신호를 받을 때까지 대기
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    <-c // 이 코드가 없으면 main이 바로 끝나버려서 goroutine도 같이 종료됨

    // 4. 신호를 받으면 여기부터 실행
    app.Shutdown()

    // 5. main 함수 종료 → 프로그램 종료
}

중요: <-c 같은 blocking 코드가 없으면 main 함수가 바로 끝나버려서 goroutine도 같이 강제 종료됩니다. 그래서 채널로 신호를 기다리면서 main을 살려두는 것입니다.

Hexagonal Architecture에서의 구현

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/gofiber/fiber/v2"
    "yourapp/internal/adapter/handler"
    "yourapp/internal/adapter/repository"
    "yourapp/internal/application/service"
)

func main() {
    // 의존성 주입
    db := initDB()
    defer db.Close()

    // Repository 계층
    chatRepo := repository.NewChatRepository(db)

    // Service 계층
    chatService := service.NewChatService(chatRepo)

    // Handler 계층
    chatHandler := handler.NewChatHandler(chatService)

    // Fiber 앱 생성 및 라우트 등록
    app := fiber.New()
    chatHandler.RegisterRoutes(app)

    // 서버 시작 (goroutine)
    go func() {
        if err := app.Listen(":3000"); err != nil {
            log.Panic(err)
        }
    }()

    // Graceful shutdown 대기
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
    <-quit

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

    // 서버 종료
    if err := app.Shutdown(); err != nil {
        log.Fatal(err)
    }

    // 추가 정리 작업 (DB 연결 등)
    // defer로 이미 처리되지만, 명시적으로 추가 가능

    log.Println("Server exited properly")
}

func initDB() *sql.DB {
    // MySQL 연결 초기화
    // ...
    return db
}

테스트 방법

  1. 서버 실행: go run main.go
  2. 종료 신호 전송: Ctrl + C 또는 kill -SIGTERM <PID>
  3. 로그 확인: "Gracefully shutting down..." → "Server shut down successfully"

마무리

원래 서버는 반복문처럼 계속 실행되다가 종료되는 구조였다면, graceful shutdown에서는:

  • 서버를 goroutine으로 백그라운드 실행
  • main 함수는 신호를 기다림
  • 종료 신호를 받으면 정리 작업 후 종료

이렇게 명시적인 종료 프로세스를 통해 안전하고 깔끔한 서버 종료가 가능합니다.

'[개발] Go' 카테고리의 다른 글

GO vs JS, Python, Java 비교  (0) 2025.05.31