programming/golang

고루틴도 편히 쉬어야 한다! Go에서 Graceful Shutdown 구현하기

jamie91 2025. 3. 10. 10:42

 

 

Go 언어로 프로그래밍할 때, 여러 고루틴이 동시에 동작하다 보면 언제, 어떻게 안전하게 종료시킬지 고민하게 된다. 단순히 프로세스를 종료하는 것이 아니라, 자원 해제, 데이터 무결성 보장, 작업 마무리 등 여러 요소를 고려해야 하는데, 이를 "Gracefully 하게 죽인다"라고 표현한다.

그럼, Gracefully 하게 죽인 다는 기준은 무엇일까?

  • 안전한 자원 해제: 열려있는 파일, 데이터베이스 연결, 네트워크 소켓 등 모든 자원을 올바르게 닫아야 한다.
  • 데이터 무결성 유지: 진행 중인 작업이나 처리 중인 데이터가 중간에 끊겨 데이터 불일치나 손실이 발생하지 않아야 한다.
  • 예측 가능한 종료: 고루틴이 종료될 때, 어떠한 예외 상황 없이 명시된 절차에 따라 종료되어야 한다.

이 글에서는 두 가지 방법을 살펴본다.


1. goroutine 내부에서 제어 소스 추가하는 방식

이 방식은 고루틴 내부에 종료 신호를 체크하는 코드를 포함시켜, 스스로 종료되도록 하는 방법이다.

예를 들어, 고루틴 내에서 주기적으로 제어 채널이나 컨텍스트를 통해 종료 요청이 들어왔는지 확인하는 구조를 사용할 수 있다.

내부 제어 방식 코드 예제

func worker(stopCh <-chan struct{}) {
    for {
        select {
        case <-stopCh:
            fmt.Println("Worker is stopping gracefully...")
            // 필요한 정리 작업 수행
            return
        default:
            // 실제 작업 수행
            fmt.Println("Working...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    stopCh := make(chan struct{})
    go worker(stopCh)

    // 3초 후 고루틴 종료 요청
    time.Sleep(3 * time.Second)
    close(stopCh)

    // 종료를 위해 잠시 대기
    time.Sleep(1 * time.Second)
}

위 예제에서는 고루틴이 주기적으로 stopCh 채널을 모니터링하며, 종료 요청이 들어오면 정리 작업 후 안전하게 종료된다.

이와 같은 패턴은 How to PROPERLY stop goroutine? [SOLVED] | GoLinuxCloud에서도 소개된 방식으로, 고루틴 내부에서 스스로 종료 조건을 판단하는 방식이다.


2. goroutine 외부에서 제어하는 방식??

외부에서 고루틴의 종료를 제어하는 방식은 고루틴 외부의 컨텍스트나 제어 로직을 통해 고루틴에 종료 신호를 전달하는 형태이다.

사실상 외부 제어 방식도 내부에서 종료 신호를 처리하게끔 고루틴 코드를 작성해야 한다는 점에서는 큰 차이가 없지만, 누군가(또는 무언가)가 외부에서 "이제 그만"이라고 명령하는 방식이라고 볼 수 있다.

외부 제어 방식 (Context 사용) 코드 예제

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker received cancellation signal and is stopping gracefully...")
            // 필요한 정리 작업 수행
            return
        default:
            // 실제 작업 수행
            fmt.Println("Working...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // 외부에서 취소 신호를 전달할 수 있는 컨텍스트 생성
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx)

    // 3초 후 고루틴 종료 요청
    time.Sleep(3 * time.Second)
    cancel()

    // 종료를 위해 잠시 대기
    time.Sleep(1 * time.Second)
}

이 예제는 context.Context를 사용하여 외부에서 고루틴에 취소 신호를 전달하는 패턴이다.

외부의 컨트롤러가 cancel()을 호출하면, 고루틴 내에서는 ctx.Done() 채널을 통해 종료 요청을 감지하고 안전하게 종료된다.

외부 제어 방식의 고려사항

  • 협력(cooperative cancellation): 고루틴이 외부에서 전달한 종료 신호를 감지할 수 있도록 코드가 설계되어 있어야 한다.
  • 강제 종료의 부작용: 만약 고루틴이 외부 제어 신호를 무시하거나, 응답하지 않는다면, 강제 종료 시 자원 누수나 불완전한 데이터 처리 문제가 발생할 수 있다.
  • 타임아웃 및 펑백: 외부 제어 시 타임아웃 로직을 추가하여, 고루틴이 너무 오래 응답하지 않을 경우 추가적인 정리 작업을 수행할 수 있도록 할 수 있다.

결론

고루틴을 Gracefully 하게 죽이는 방법은 결국 고루틴 내부에 종료 조건을 체크하는 로직을 포함하는 것이다.

내부에서 스스로 종료하도록 설계하는 방식과 외부에서 컨텍스트나 채널을 통해 종료 요청을 전달하는 방식은 큰 틀에서는 동일한 원칙(협력적 종료)을 따른다.

그렇다면 Gracefully 하게 죽인 다는 기준은, 고루틴이 종료되기 전에 모든 필요한 정리 작업을 수행하고, 자원을 올바르게 해제하며, 데이터 무결성을 유지하는 것이다.

이러한 패턴을 잘 활용하면, Go 애플리케이션에서 발생할 수 있는 고루틴 누수를 예방하고, 안정적이고 예측 가능한 종료를 보장할 수 있다.

 

 

728x90
반응형