티스토리 뷰

🪓 우아하지 않은 종료

 다음과 같이 모든 요청에 "hello world"로 응답하는 간단한 http 서버가 있습니다. 코드의 실행 순서는 다음과 같습니다.

 

  1. DB 연결
  2. http 서버 시작
  3. http 서버 종료
  4. DB 연결 종료
package main

import (
	"context"
	"database/sql"
	"log"
	"net/http"

	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// 1. DB 연결
	db, err := ConnectDB()
	if err != nil {
		log.Fatal(err)
	}

	log.Println("DB connection established")

	srv := &Server{
		Server: &http.Server{
			Addr: ":8080",
			Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.Write([]byte("Hello World"))
			}),
		},
		db: db,
	}

	log.Println("Starting server...")

	// 2. 서버 시작
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}

	// 3. 서버 종료
	log.Println("Shutting down server...")
	if err := srv.Shutdown(context.Background()); err != nil {
		log.Fatal(err)
	}

	// 4. DB 연결 종료
	if err := db.Close(); err != nil {
		log.Fatal(err)
	}

	log.Println("DB connection closed")

	log.Println("Server shutdown complete")
}

type Server struct {
	*http.Server
	db *sql.DB
}

func ConnectDB() (*sql.DB, error) {
	db, err := sql.Open("sqlite3", "./sql.db")
	if err != nil {
		return nil, err
	}

	if err := db.Ping(); err != nil {
		return nil, err
	}

	return db, nil
}

 순서대로 모든 동작이 실행될 것이라고 예상하고 코드를 실행해 보면 일단 다음과 같이 서버가 시작되었다는 로그까지 출력이 됩니다.

$ go run main.go 
2023/10/24 13:44:29 DB connection established
2023/10/24 13:44:29 Starting server...

 그런데 Ctrl + C (SIGINT)를 눌러서 프로세스를 종료하게 되면 'signal: interrupt'라는 문구만 뜨고 3번과 4번이 정상적으로 실행되지 않은 것을 확인할 수 있습니다.

$ go run main.go 
2023/10/24 13:44:29 DB connection established
2023/10/24 13:44:29 Starting server...
^Csignal: interrupt

 도커 이미지를 빌드하여 컨테이너로 실행한 뒤, 정지(SIGTERM)해도 3번과 4번이 실행되지 않는 것은 마찬가지입니다.

$ docker build -t server .
$ docker run --name server -d -p 8080:8080 server
$ docker stop server
$ docker logs server
2023/10/24 05:29:53 DB connection established
2023/10/24 05:29:53 Starting server...

 이런식으로 운영체제로부터 SIGINT 또는 SIGTERM 등의 시그널을 받아서 프로세스를 종료할 때 주어진 종료 절차를 제대로 실행하지 않고 허겁지겁 종료해 버리는 것은 자원 누수나 연결 제한 등의 문제를 야기할 수 있습니다.


🩰 우아하게 종료하기

 프로세스를 우아하게 종료하기 위해서는 운영체제로부터 전달되는 시그널을 받아서 처리할 수 있어야 합니다. Go에는 이를 위한 "os/sygnal" 내장 패키지가 존재합니다.

 

GracefulShutdown 함수

 다음 함수는 운영체제로부터 특정 시그널을 받기 전까지는 닫히지 않고 빈 구조체를 전달하는 채널을 반환합니다.

func GracefulShutdown(fn func(), sigs ...os.Signal) <-chan struct{} {
	stop := make(chan struct{})
	sigChan := make(chan os.Signal, 1)

	signal.Notify(sigChan, sigs...)

	go func() {
		<-sigChan

		signal.Stop(sigChan)

		fn()

		close(sigChan)
		close(stop)
	}()

	return stop
}

 signal.Notify 함수는 가변인자로 os.Signal 타입의 값들을 받아 운영체제로부터 전달받는 시그널 중 어떤 것을 주어진 채널(sigChan)로 전달할지 지정합니다.

signal.Notify(sigChan, sigs...)

 그리고 시그널이 sigChan으로 전달되면 GracefulShutdown 함수에서 받은 fn 함수를 실행하고 sigChan 채널과 stop 채널을 닫아 줍니다.

go func() {
	<-sigChan

	fn()

	close(sigChan)
	close(stop)
}()

 

GracefulShutdown 함수 적용하기

 다음은 main 함수에서 GracefulShutdown 함수를 적용한 코드입니다. GracefulShutdown 함수의 첫 번째 인수로 들어가는 함수에는 서버가 종료될 때 실행되어야 하는 종료 절차가 들어가 있습니다. 그리고 뒤에는 두 가지의 시그널, SIGINT와 SYGTERM을 지정하여 이 시그널이 운영체제로부터 전달되었을 때 프로세스를 종료하도록 하였습니다. 그리고 또 중요한 부분은 GracefulShutdown 함수에서 반환되는 채널을 사용해 main 함수가 종료되지 않도록 블로킹을 해주는 것입니다.

func main() {
	// 1. DB 연결
	db, err := ConnectDB()
	if err != nil {
		log.Fatal(err)
	}

	log.Println("DB connection established")

	srv := &Server{
		Server: &http.Server{
			Addr: ":8080",
			Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.Write([]byte("Hello World"))
			}),
		},
		db: db,
	}

	log.Println("Starting server...")

	go func() {
		// 2. 서버 시작
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	stop := GracefulShutdown(func() {
		// 3. 서버 종료
		log.Println("Shutting down server...")
		if err := srv.Shutdown(context.Background()); err != nil {
			log.Fatal(err)
		}

		// 4. DB 연결 종료
		if err := db.Close(); err != nil {
			log.Fatal(err)
		}

		log.Println("DB connection closed")

		log.Println("Server shutdown complete")
	}, syscall.SIGINT, syscall.SIGTERM)

	<-stop
}

서버 시작을 고루틴으로 감싼 이유

 srv.ListenAndServe 메서드는 무한 루프로 실행되어 오류가 발생하기 전까지는 err에 값을 할당하지 않기 때문에 이후에 오는 코드가 실행되지 못하게 블로킹하게 됩니다. 따라서 GracefulShutdown 함수를 정상적으로 실행되기 위해서 서버 시작 부분을 고루틴으로 감쌌습니다.

go func() {
	// 2. 서버 시작
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}()

 

GracefulShutdown 함수를 적용한 main 함수 실행

 이제 코드를 실행한 뒤, Ctrl + C를 눌러서 종료시키면 다음과 같이 종료 절차가 정상적으로 실행되는 것을 확인할 수 있습니다.

$ go run ./
2023/10/24 15:32:13 DB connection established
2023/10/24 15:32:13 Starting server...
^C2023/10/24 15:32:18 Shutting down server...
2023/10/24 15:32:18 DB connection closed
2023/10/24 15:32:18 Server shutdown complete

 도커 컨테이너를 실행하고 정지했을 경우에도 정상적으로 종료 절차가 실행되는 것을 확인할 수 있습니다.

$ docker build -t server .
$ docker run --name server -d -p 8080:8080 server
$ docker stop server
$ docker logs server
2023/10/24 06:36:08 DB connection established
2023/10/24 06:36:08 Starting server...
2023/10/24 06:36:12 Shutting down server...
2023/10/24 06:36:12 DB connection closed
2023/10/24 06:36:12 Server shutdown complete
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Total
Today
Yesterday
글 보관함