Go 서비스가 WaitGroup 에서 멈춘 것처럼 보일 때 문제는 대개 WaitGroup 자체가 아닙니다. 더 흔한 경우는 Done 에 도달하지 못하는 worker, 영원히 return 하지 않는 blocking path, 혹은 error / panic / cancellation 경로에서 깨지는 Add / Done 균형입니다.
짧게 말하면 핵심은 이것입니다. 어떤 goroutine 이 끝나지 않고 있는지, 그리고 그 Done 경로가 실제로 아직 reachable 한지 먼저 찾아야 합니다. stuck WaitGroup 은 거의 항상 counter 가 0이 되지 않았다는 뜻입니다.
Java 전체 대신 Go 쪽 더 넓은 흐름부터 보고 싶다면 Golang Troubleshooting Guide 를 먼저 봐도 좋습니다.
Add 와 Done 균형부터 본다
WaitGroup 버그는 대부분 accounting bug 나 lifecycle bug 입니다.
그래서 아래 질문이 먼저입니다.
Add는 어디서 호출되는가Done은 어디서 호출되는가- 어떤 경로가
Done을 건너뛸 수 있는가 - 어떤 worker 가 return 하지 못하고 있는가
이걸 답하기 전까지 WaitGroup 자체는 대개 흥미로운 부분이 아닙니다.
stuck WaitGroup 은 실전에서 어떻게 보이나
운영 환경에서는 보통 이렇게 보입니다.
- shutdown 이 끝나지 않는다
- 한 request path 가 끝나지 않는 worker 를 계속 기다린다
- 거의 끝난 것처럼 보이는데 hidden goroutine 하나가 남아 있다
- 운영자는 deadlock 을 의심하지만 실제론 missing
Done이다
겉으로 보이는 증상은 “Wait 이 안 끝난다” 이지만, 실제 원인은 훨씬 구체적인 경우가 많습니다.
흔한 원인
1. Done 이 호출되지 않는다
error branch, timeout branch, panic path 로 worker 가 빠져나가면서 counter 감소가 빠질 수 있습니다.
가장 흔한 원인입니다.
2. Add 가 잘못된 위치에서 호출된다
너무 늦게 Add 하거나, 안전하지 않은 concurrent path 에서 호출하면 wait 동작이 매우 헷갈려질 수 있습니다.
Go 에서는 worker launch 대비 Add 위치가 중요합니다.
3. worker 하나가 영원히 block 된다
channel wait, network call, DB call, dependency stall 때문에 goroutine 하나가 계속 살아 있으면서 전체 wait 를 막을 수 있습니다.
4. shutdown 과 cancellation 이 불완전하다
context cancellation 때 멈춰야 하는 worker 가 계속 기다리면, 실제 문제는 lifecycle coordination 인데 WaitGroup 이 고장 난 것처럼 보일 수 있습니다.
5. panic 또는 early-return 경로가 예상 경로를 우회한다
happy path 는 맞아도 unusual exit branch 에서 Done 보장이 깨질 수 있습니다.
실전 점검 순서
1. 어떤 wait path 가 막혔고 어떤 worker 가 아직 살아 있는지 확인한다
첫 단계는 counter 를 0으로 못 내리는 goroutine 을 찾는 것입니다.
2. stuck path 주변의 모든 Add / Done 쌍을 점검한다
기억에 의존하지 말고 실제 control flow 를 따라가야 합니다.
3. blocked channel operation 또는 downstream call 이 있는지 본다
worker 가 return 하지 않으면 defer wg.Done() 이 있어도 실제로는 도움이 안 됩니다.
4. panic, timeout, cancellation branch 에서도 Done 이 보장되는지 확인한다
운영에서 많이 터지는 WaitGroup 버그가 여기 숨어 있습니다.
5. 최근 concurrency 변경과 같이 본다
새 fan-out, 새 shutdown logic, 바뀐 cancellation rule 이 WaitGroup 이 갑자기 sticky 해진 이유일 수 있습니다.
예시: defer 는 맞지만 lifecycle 이 틀린 경우
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if err := doWork(ctx); err != nil {
return
}
}()
wg.Wait()
겉보기엔 안전합니다. 실제로도 자주 맞습니다. 하지만 doWork(ctx) 가 영원히 return 하지 않으면 wg.Done() 은 실전에서는 도달 불가능합니다.
그래서 defer wg.Done() 은 가장 안전한 패턴이지만, 그 자체로 충분한 해답은 아닙니다.
stuck path 를 찾은 뒤 무엇을 바꾸면 좋나
Done 이 빠질 수 있다면
모든 exit path 에서 Done 이 보장되도록 worker 구조를 다시 짜야 합니다.
Add 가 unsafe 하다면
worker launch 전에 accounting 이 끝나도록 위치를 옮겨야 합니다.
worker 가 영원히 block 된다면
blocking condition, timeout path, dependency behavior 를 먼저 고쳐야 합니다.
cancellation 이 불완전하다면
worker shutdown 을 명시적으로 만들어서 Wait 가 실제 lifecycle completion 을 반영하게 해야 합니다.
실제 문제가 leaked work 라면
WaitGroup bug 보다 goroutine lifecycle 문제로 다뤄야 합니다.
장애 중에 던져볼 질문
이 질문이 거의 항상 유용합니다.
WaitGroup counter 를 0보다 크게 유지하는 정확한 goroutine 은 무엇이고, 그 goroutine 이 exit 하려면 어떤 조건이 만족돼야 하는가?
이 질문이 real bug 로 거의 바로 이어집니다.
FAQ
Q. defer wg.Done() 이면 항상 충분한가
대개 가장 안전한 패턴이지만, worker 가 return 하지 않으면 여전히 안 끝납니다.
Q. 가장 빠른 첫 단계는 무엇인가
어떤 goroutine 이 아직 살아 있고, 그 Done 경로가 reachable 한지 찾는 것입니다.
Q. stuck WaitGroup 은 항상 deadlock 인가
아닙니다. missing accounting 이나, 다른 무언가를 영원히 기다리는 worker 인 경우가 많습니다.
Q. context cancellation 이 도움이 되나
그렇지만 worker 가 그 signal 을 실제로 존중하고 clean 하게 exit 해야만 합니다.
Read Next
- blocked worker 가 계속 쌓이는 유형이라면 Golang Goroutine Leak 을 이어서 보세요.
- cancellation 이 너무 빨리 닫히는 문제와 함께 보인다면 Golang Context Cancelled Too Early 와 비교해 보세요.
- queueing 과 worker coordination 이 더 넓은 문제라면 Golang Worker Pool Backpressure 를 같이 보세요.
- 전체 Go 분기 지도는 Golang Troubleshooting Guide 에서 이어 볼 수 있습니다.
Related Posts
- Golang Goroutine Leak
- Golang Context Cancelled Too Early
- Golang Worker Pool Backpressure
- Golang Troubleshooting Guide
Sources:
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.