Golang goroutine 안에서 panic이 날 때
마지막 업데이트

Golang goroutine 안에서 panic이 날 때


goroutine 안에서 panic이 나면 겉보기에는 랜덤하게 보일 수 있지만, 실제 문제는 보통 랜덤이 아닙니다. recover 경계가 어디에 없는지, worker 코드가 invalid state를 어떻게 받아들이는지, background failure가 얼마나 보이는지가 핵심입니다.

그래서 goroutine panic은 체감상 더 고통스럽습니다. obvious request path 밖에서 일어나기 쉽고, 생각보다 더 넓은 작업을 깨뜨릴 수 있으며, queue 적체나 로그 누락, 부분 장애가 먼저 나타난 뒤에야 발견되는 경우도 많기 때문입니다.

이 글은 실전 순서에 집중합니다.

  • panic 경계를 어떻게 찾을지
  • worker 격리 문제와 더 깊은 로직 문제를 어떻게 나눌지
  • goroutine panic이 작업을 깨뜨릴 때 무엇을 먼저 볼지

짧게 말하면 panic이 어디서 catch되거나 catch되지 않는지 먼저 확인하고, 각 goroutine 경계에 적절한 recover/report 동작이 있는지 본 뒤, 어떤 invalid state나 코드 경로가 panic을 계속 만드는지 추적해야 합니다.

더 넓은 Go 분기부터 다시 보고 싶다면 Golang 트러블슈팅 가이드로 가세요.


먼저 panic 경계부터 보기

가장 먼저 물어볼 질문은 panic이 어디에서 멈추는가입니다.

이 답은 핵심 문제가 아래 중 어디에 가까운지 보여줍니다.

  • 한 worker에 recovery가 없는 문제
  • top-level goroutine 경계에 reporting이 없는 문제
  • 계속 invalid state를 만드는 더 깊은 로직 문제

이 분기 없이 보면 generic한 recover를 아무 데나 추가하고, 실패를 어디서 격리하고 보고해야 하는지라는 더 중요한 질문을 놓치기 쉽습니다.


worker 격리와 process-wide damage의 차이

모든 goroutine panic의 blast radius가 같은 것은 아닙니다.

아래를 물어보세요.

  • panic이 한 worker만 종료시키는가, 더 넓은 프로세스 경로를 건드리는가
  • panic 로그에 failing path를 찾을 만큼 맥락이 남는가
  • 실패한 worker를 안전하게 멈추거나 재시작하는가
  • 같은 invalid input이 들어와 반복적으로 panic이 나는가

이 질문이 중요한 이유는, “recover를 넣었다”가 곧 “안전한 실패”를 뜻하지 않기 때문입니다. 조용히 recover하지만 관측이 사라진 worker도, 크게 죽는 worker만큼 위험할 수 있습니다.


자주 나오는 원인

1. 적절한 경계에 recover가 없다

worker panic이 생각보다 멀리 전파되는 이유는, goroutine 경계에 recovery와 reporting 전략이 없기 때문인 경우가 많습니다.

이 패턴은 보통 아래에서 자주 나옵니다.

  • background worker launch
  • queue consumer goroutine
  • handler 깊은 곳에서 시작한 helper goroutine

항상 recover가 전부 없어서가 아니라, 실패를 격리해야 할 정확한 경계에 없어서 문제가 되는 경우가 많습니다.

2. shared worker code가 유효 상태를 과하게 가정한다

unexpected nil, invalid state, stale assumption 때문에 같은 panic이 반복될 수 있습니다.

예를 들면:

  • partial setup 뒤 nil pointer dereference
  • 동시성 하에서 더 이상 맞지 않는 map, slice 가정
  • dependency 응답에 대한 위험한 가정

같은 panic이 계속 반복된다면, 더 깊은 원인은 panic handling이 아니라 worker 코드까지 invalid state가 들어오는 경로일 수 있습니다.

3. background task 실패가 잘 보이지 않는다

panic이 obvious request path 밖에서 나기 때문에 관측이 약해지기 쉽습니다.

그래서 팀은 가끔 아래 증상부터 먼저 봅니다.

  • job이 조용히 처리되지 않기 시작함
  • worker pool이 서서히 worker를 잃음
  • panic을 일으킨 input과 로그가 잘 연결되지 않음

이 경우 문제는 panic 하나가 아니라, panic 경계 주변의 visibility 부족까지 포함됩니다.


실전 점검 순서

goroutine panic이 보일 때는 아래 순서가 가장 도움이 됩니다.

  1. panic이 catch되는지 여부 확인
  2. failing work를 시작한 goroutine 경계 확인
  3. 반복되는 nil / invalid state / stale assumption 경로 점검
  4. 최근 동시성 변경이나 lifecycle 변경과 panic timing 비교
  5. recover 문제인지, validation 문제인지, worker ownership 문제인지 판단

이 순서가 중요한 이유는 두 가지 흔한 실수를 막기 때문입니다.

  • failing path를 모른 채 broad recover부터 추가하는 실수
  • panic 지점만 보고 그 invalid state가 왜 그 goroutine까지 왔는지 놓치는 실수

blocked 또는 stuck goroutine도 함께 보인다면 Goroutine leak 찾는 법과 같이 보세요.


아주 작은 예시가 보여주는 핵심

go func() {
	panic("worker failed")
}()

background goroutine 안의 panic도 recover, reporting, safe stop 경로가 없으면 생각보다 더 넓은 문제를 만들 수 있습니다.

중요한 것은 “여기서 panic이 가능한가?”보다 “panic이 나면 여기서 무엇이 일어나야 하는가?”입니다.


조금 더 안전한 경계는 어떤 모습인가

더 안전한 goroutine 경계에는 보통 아래가 포함됩니다.

  • 로컬 defer + recover
  • failing path를 찾을 수 있는 충분한 context logging
  • worker를 멈추거나 재시작하는 명시적 정책

이 말이 모든 panic을 삼키라는 뜻은 아닙니다. 장수 worker 경계마다 실패 전략이 의식적으로 있어야 한다는 뜻입니다.

그렇지 않으면 시스템은 조용한 worker death와 시끄러운 process failure 사이를 오가게 됩니다.


go func()마다 물어볼 질문

명시적으로 띄우는 각 goroutine마다 아래를 물어보면 도움이 됩니다.

  • 이 goroutine 안에서 어떤 failure가 날 수 있는가
  • 그 failure는 누가 관측하는가
  • panic이 나면 주변 시스템은 어떻게 되어야 하는가
  • 지금 코드가 실제로 그렇게 동작하는가

이 프레이밍이 좋은 이유는 많은 panic 장애가 사실 ownership과 observability 장애이기 때문입니다.


FAQ

Q. 모든 goroutine에 다 recover를 넣어야 하나요?

무조건 그렇지는 않습니다. 더 중요한 질문은 그 goroutine 경계가 failure를 격리해야 하는지, 그리고 failure를 어떻게 보고하거나 escalation해야 하는지입니다.

Q. background goroutine panic이 왜 랜덤하게 느껴지나요?

obvious request path 밖에서 일어나고, context와 로그가 약하고, 증상이 나중에 드러나는 경우가 많기 때문입니다.

Q. 운영에서 무엇부터 보는 게 가장 빠른가요?

panic 경계를 찾고, failure가 한 worker에 갇혔는지 process-wide였는지 확인한 뒤, 그 goroutine까지 invalid state가 어떻게 들어왔는지 보세요.


Sources:

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.