Graceful Shutdown이란?
Graceful Shutdown은 애플리케이션이 종료될 때, 현재 처리 중인 작업을 안전하게 마무리하고 사용 중인 리소스를 정리하며, 더 이상 새로운 요청을 받지 않도록 하는 프로세스를 의미합니다.
실제 서비스 운영 환경에서는 배포를 위해 서버를 재시작하거나, 장애 복구를 위해 프로세스를 종료해야 하는 상황이 자주 발생합니다. 이때 단순히 프로세스를 강제로 종료시키면 진행 중이던 요청이 끊기거나, 외부 시스템과의 연결이 비정상적으로 종료되며, 데이터 손실까지 발생할 수 있습니다. 이러한 문제를 방지하기 위해 Golang에서는 Graceful Shutdown 패턴을 활용하여 안전한 종료 절차를 구현할 수 있습니다.
Graceful Shutdown의 필요성
실제 서비스 운영 환경에서는 배포를 위해 서버를 재시작하거나, 장애 복구를 위해 프로세스를 종료해야 하는 상황이 자주 발생합니다. 이때 단순히 프로세스를 강제로 종료시키면 진행 중이던 요청이 끊기거나, 외부 시스템과의 연결이 비정상적으로 종료되며, 데이터 손실까지 발생할 수 있습니다. 이러한 문제를 방지하기 위해 Golang에서는 Graceful Shutdown(우아한 종료) 패턴을 활용하여 안전한 종료 절차를 구현합니다.
- 외부에서 데이터를 수신한다.
- 수신한 데이터를 가공한다.
- 가공된 데이터를 데이터베이스에 저장한다.
이런 흐름에서 Graceful Shutdown이 구현되어 있지 않다면 문제가 발생할 수 있습니다. 애플리케이션이 데이터를 수신한 직후, 가공 작업이나 저장 작업이 수행되는 중간에 종료 신호(SIGTERM 등)가 들어오면, 해당 작업이 끝까지 처리되지 못한 채 중단됩니다. 이로 인해 가공 중이던 데이터가 유실되거나, 데이터베이스에 반영되어야 할 정보가 정상적으로 저장되지 못해 데이터가 불일치하는 문제가 발생할 수 있습니다. 그러므로 Graceful Shutdown은 서비스 안정성, 데이터 무결성, 외부 시스템과의 정상적인 종료 절차를 보장하기 위한 중요한 기능이라고 할 수 있습니다.

Graceful Shutdown 구현 방법
Graceful Shutdown 구현 방법에 대해서는 제가 직접 개발한 Kubernetes Event Collector 프로젝트의 코드를 예시로 설명드리겠습니다. 이 프로젝트에서는 Kubernetes 클러스터에서 발생하는 이벤트를 수집하여 외부 시스템으로 전송하는 기능을 수행합니다. Graceful Shutdown을 구현하기 위해 다음과 같은 단계를 거쳤습니다.
프로젝트 링크 : https://github.com/k8shuginn/event-collector
GitHub - k8shuginn/event-collector
Contribute to k8shuginn/event-collector development by creating an account on GitHub.
github.com
cmd/collector/app/app.go
// Run Collector 실행
func (c *Collector) Run() {
logger.Info("kubernetes event collector started ...")
defer logger.Info("kubernetes event collector stopped ...")
wg := sync.WaitGroup{}
// 시그널 수신 설정
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// event 수집 시작
ctx, cancel := context.WithCancel(context.Background())
for _, e := range c.exporters {
go e.Start(ctx, &wg)
}
c.k8sClient.Run()
// 1. 종료 시그널 발생
<-sigChan
// 2. 더 이상 이벤트 수신 안함
c.k8sClient.Close()
// 3. Exporter들에게 종료 신호 전달
cancel()
// 4. 모든 Exporter 종료 대기
exitChan := make(chan struct{})
go func() {
defer close(exitChan)
wg.Wait()
}()
select {
case <-exitChan:
logger.Info("all exporters stopped")
return
// 5. 모든 Exporter가 10초 이내에 종료되지 않으면 강제 종료
case <-time.After(DefaultTimeout):
logger.Warn("timeout waiting for exporters to stop")
return
}
}
동작 과정
- 시그널 수신 설정: os/signal 패키지를 사용하여 SIGINT와 SIGTERM 시그널을 수신할 채널을 이용하여 종료 시그널이 발생할 때까지 대기합니다.
- 이벤트 수집 종료: 종료 시그널이 수신되면 Kubernetes 클라이언트의 Close 메서드를 호출하여 더 이상 이벤트를 수신하지 않도록 합니다.
- Exporter 종료 신호 전달: context.WithCancel을 사용하여 Exporter들에게 종료 신호를 전달합니다.
- Exporter 종료 대기: sync.WaitGroup을 사용하여 모든 Exporter가 종료될 때까지 대기합니다.
- 타임아웃 설정: 모든 Exporter가 10초 이내에 종료되지 않으면 강제로 종료합니다.
각각의 Exporter 코드
// Start exporter 시작
func (e *ElasticsearchExporter) Start(ctx context.Context, wg *sync.WaitGroup) error {
logger.Info("[elasticsearch exporter] started")
ticker := time.NewTicker(5 * time.Second)
// 2. 종료 시그널 수신 시 안전하게 종료하기 위한 리소스 정리 및 버퍼 플러시
defer func() {
close(e.dataChan) // 데이터 채널 닫기
ticker.Stop() // 티커 중지
e.shutdown() // 리소스 정리
logger.Info("[elasticsearch exporter] stopped")
wg.Done()
}()
wg.Add(1)
for {
select {
// 1. context.WithCancel을 이용한 종료 신호를 수신
case <-ctx.Done():
return nil
case data := <-e.dataChan:
e.writeBuffer(data)
if len(e.buffer) >= e.flushSize {
e.writeBulk()
}
case <-ticker.C:
if len(e.buffer) > 0 {
e.writeBulk()
}
}
}
}
동작 과정
- 종료 신호 수신: context.WithCancel을 통해 전달된 종료 신호를 수신합니다.
- 리소스 정리 및 버퍼 플러시: 종료 신호가 수신되면 데이터 채널을 닫고, 티커를 중지하며, 남아있는 버퍼 데이터를 모두 전송하는 등의 리소스 정리 작업을 수행합니다.
이와 같은 방식으로 Golang 애플리케이션에서 Graceful Shutdown을 구현하면, 서비스 종료 시에도 안정적으로 현재 작업을 마무리하고 리소스를 정리할 수 있습니다. 이를 통해 데이터 손실이나 불일치 문제를 방지하고, 외부 시스템과의 정상적인 종료 절차를 보장할 수 있습니다.
'Go언어' 카테고리의 다른 글
| 안전하게 개발하기 레츠GO! #5 실전 테스트 (0) | 2025.11.23 |
|---|---|
| 안전하게 개발하기 레츠GO! #3 go benchmark (0) | 2025.11.20 |
| 안전하게 개발하기 레츠GO! #2 go testing (0) | 2025.11.19 |
| 안전하게 개발하기 레츠GO! #1 디버깅 (0) | 2025.11.18 |
| Go언어의 benchmark와 test (0) | 2025.11.18 |