Go panic 스택트레이스를 변수로 저장하기 — runtime/debug.Stack()
1. 문제 정의
Go 프로그램이 panic을 일으키면 런타임이 기본적으로 다음과 같은 형태의 스택트레이스를 **표준 출력(stdout)**으로 출력한다.
| |
여기서 핵심은 recover()를 사용해 패닉을 회복할 때다. recover()는 스택트레이스를 전혀 반환하지 않고 패닉의 원인을 설명하는 값만 돌려준다.
| |
이 문자열만으로는 “어느 파일의 몇 번째 라인에서 패닉이 났는지"를 알 수 없다. 실제 운영 환경에서 에러 로그를 수집하거나 버그 리포트를 남길 때, 원인 문자열만 저장하면 재현할 수 없는 문제가 된다. 이 글에서 다루는 질문은 다음과 같다.
stdout으로 출력되는 패닉 스택트레이스를 변수에 저장할 수 있는가? (출처: StackOverflow 52103182)
2. 표면 증상
격리해 보면 두 가지 증상이 나타난다.
| 증상 | recover()만 사용했을 때 | debug.Stack()을 함께 사용했을 때 |
|---|---|---|
| 반환되는 정보 | 패닉 원인 값 (예: runtime error: index out of range) | 패닉 원인 값 + 전체 스택트레이스 |
| 패닉 발생 지점 | 알 수 없음 (/tmp/.../main.go:9) | 파일명·라인 번호 포함 (main.go:9 +0x20) |
| 디버깅 가치 | 낮음 (무엇이 났는지만 알 수 있음) | 높음 (어디서 났는지 정확히 알 수 있음) |
사용자가 쉽게 오해하는 지점은 “recover()가 반환하는 값이 곧 패닉 정보의 전부"라고 생각하는 것이다. 실제로 recover()는 패닉이 전달한 원인 값일 뿐이고, 스택 정보는 별도로 가져와야 한다.
빠른 판단 기준: 패닉 발생 위치(파일·라인)가 필요하면 debug.Stack()을, 원인 값만 필요한 로직 처리면 recover()만 사용한다.
3. 증상 fingerprint
- 에러 메시지 형태:
panic: runtime error: index out of range(stdout에 자동 출력) - 발생 단계: 런타임 패닉 발생 직후의 defer 회복 핸들러
- 관련 도구/프레임워크: Go 런타임 (
runtime패키지,runtime/debug패키지) - 핵심 저수준 원리:
recover()는panic에 전달된 값을 반환하며, 현재 실행 중인 goroutine의 콜 스택(frame) 정보는runtime/debug.Stack()이 별도로 제공한다. - 흔한 오해: “recover() 값에 스택트레이스가 포함되어 있다”, 또는 “panic이 stdout으로 찍은 트레이스를 파일로 리다이렉트하면 된다”
4. 원인 추적
왜 recover()만으로는 위치 정보를 얻을 수 없는지 Go 런타임 내부 관점에서 짚어 보자.
- panic이 발생하면 런타임은 현재 goroutine의 스택 프레임을 역추적(backtrace) 해서 스택트레이스를 만들고, 기본 팬 에러 핸들러가 이를 stdout으로 출력한다.
recover()를 설치한 defer 함수는 panic 상황을 중간에 가로채지만, 이때recover()가 노출하는 것은panic에 넘겨진 인자 값뿐이다. 스택 프레임 정보는 별도로 구성된다.- 따라서 패닉이 실제로 터진 파일·라인을 알려면, 스택 프레임 정보를 직접 조회해야 한다. 이때 사용하는 API가
runtime/debug의debug.Stack()이다.
debug.Stack()은 호출 시점의 현재 goroutine 스택트레이스를 []byte로 반환한다. defer 핸들러 안에서 호출하면, 그 시점에 아직 unwind되기 전의 스택이므로 패닉 지점(예: main.main() ... main.go:16)이 트레이스에 포함된다.
5. 근본 원인
질문의 근본 원인은 두 정보 채널이 분리되어 있다는 데 있다.
- 채널 1 —
recover()의 반환값: 패닉의 원인(값)만 담긴다. - 채널 2 — 런타임 스택트레이스: 콜 스택(frame) 정보로, 기본 출력 또는
debug.Stack()을 통해 접근 가능하다.
이 둘을 함께 묶어야만 “무엇이 났는지 + 어디서 났는지"를 모두 남길 수 있다. recover() 값만 저장하는 것은 원인 문자열만 남기고 패닉 위치 정보를 버리는 것과 같다.
6. 해결책
질문에서 채택된 해결책은 runtime/debug 패키지의 debug.Stack()을 defer 회복 핸들러 안에서 호출해, 반환된 []byte를 string으로 변환해 변수에 저장하는 것이다.
| |
위 코드를 실행하면 (출처의 Go Playground 검증 결과 기준) 다음과 같은 출력이 얻어진다.
| |
트레이스에서 main.main() ... /tmp/sandbox973508195/main.go:16 +0x60 라인이 panic이 실제로 발생한 지점임을 정확히 확인할 수 있다. 이 문자열을 변수에 담았으므로, 이를 로그 파일에 기록하거나 에러 리포팅 시스템으로 전송할 수 있다.
원인 후보 매트릭스
| 원인 후보 | 확인 방법 | 맞는 경우 관찰되는 증상 | 해결 방향 |
|---|---|---|---|
| recover()만으로 위치를 얻으려 했음 | recover() 반환값만 로그에 저장 | 로그에 runtime error: index out of range만 있고 파일·라인 없음 | debug.Stack()을 함께 호출 |
| 스택트레이스를 수동으로 stdout 리다이렉트 영역에 의존 | panic 시 stdout을 그대로 두고 파일로 저장 | stdout과 프로덕션 로그가 분리되어 정보 유실 | defer 핸들러에서 debug.Stack()으로 직접 캡처 |
| 다른 goroutine의 스택이 필요했음 | debug.Stack()은 현재 goroutine만 반환 | 다른 goroutine 지점이 트레이스에 안 보임 | 필요 시 runtime.Stack(buf, true)로 전체 goroutine 조회 |
7. 잘못된 해결책과 주의사항
recover() 값만 로그에 남기기 — 원인 문자열만 남고 패닉 지점을 알 수 없어 재현이 어렵다. 최소한
debug.Stack()을 함께 저장해야 한다.panic이 stdout으로 찍은 트레이스를 별도 로그 파일 리다이렉트에 의존 — 프로덕션에서 stdout과 애플리케이션 로그가 분리되면 정보 유실로 이어진다. 패닉 트레이스는 코드에서 직접 캡처해 명시적으로 로그·리포팅 파이프라인에 넘기는 것이 안정적이다.
debug.Stack()을 defer가 아닌 곳에서 호출 — 패닉이 이미 unwind된 이후에는 해당 goroutine의 스택을 온전히 얻지 못할 수 있다. 반드시 defer 회복 핸들러 안에서 호출해야 한다.
주의: 위 “잘못된 해결책” 항목의 위험성은 일반적인 Go 패닉 처리 관행에 대한 점검 기준이며, 환경(로깅 프레임워크, 배포 방식)에 따라 구체적 영향은 달라질 수 있다.
8. 검증 방법
패닉 위치 정보가 제대로 저장되는지 확인하는 명령은 다음과 같다. (Go가 설치된 환경 기준)
| |
또한 저장된 스택 문자열이 실제 에러 로그에 기록되는지 확인하려면 다음처럼 간단히 검증한다.
| |
5-1. 검증 환경
이 글에서 인용한 실행 출력은 출처(StackOverflow 52103182)에 포함된 Go Playground 환경의 검증 결과다.
| 항목 | 값 |
|---|---|
| 실행 환경 | Go Playground (출처 원문 제공) |
| Go 버전 | Playground 기본(원문 작성 시점 기준, 버전 정보 공개 안 됨) |
| 패닉 유발 코드 | var mySlice []int; j := mySlice[0] |
| 확인 항목 | debug.Stack()이 패닉 발생 지점(파일·라인)을 포함하는지 |
환경에 따라 Go 버전이 다르면
runtime/debug의 내부 프레임 주소값·라인 번호는 달라질 수 있다. 핵심 동작(스택트레이스 캡처)은 동일하다.
5-2. 재현 절차
- 위 6번 코드를
main.go로 저장한다. go run main.go를 실행한다.- defer 회복 핸들러가 동작해
panic value와stacktrace from panic:뒤의 트레이스가 출력되는지 확인한다. - 트레이스 마지막 프레임(
main.main() ... main.go:16)이 실제 패닉 발생 라인인지 확인한다.
5-3. 실제 재현 결과
출처의 검증 결과에서 debug.Stack() 호출 시점의 트레이스가 다음과 같이 출력된다.
| |
main.go:16이 panic이 실제로 터진 위치로, recover() 값만으로는 얻을 수 없는 정보다.
6-1. 해결책 비교
| 접근 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
| recover() 값만 저장 | 코드가 간단 | 패닉 지점(파일·라인) 정보 없음 | 원인 값만 필요한 단순 로직 |
| debug.Stack()으로 string 저장 (채택) | 파일·라인 포함, 변수로 리포팅 가능 | recover 핸들러 안에서 호출 필요 | 에러 리포팅·로그에 위치 정보가 필요할 때 |
| runtime.Stack(buf, true) | 전체 goroutine 스택 조회 | buf 관리·성능 오버헤드 | 크래시 덤프·전체 스레드 상태 분석 |
6-2. 버전별 차이
runtime/debug.Stack()은 Go 초기 버전부터 제공되는 안정 API이다. (출처에서는 버전 명시 없음)- Go 1.12 이후 런타임은 패닉 시 프로세스가 완전히 죽지 않고도 defer에서
recover()로 회복할 수 있는 동작을 유지한다. (일반적인 런타임 동작) - 최신 Go에서 런타임 내부 라인 번호(예:
runtime/panic.go의 실제 라인)는 버전마다 달라질 수 있지만, 애플리케이션 프레임(main.go:16)은 소스 기준으로 일관되게 표시된다. 참고: 위 버전 관련 서술은 Go 일반 관행에 대한 범용 지식이며, 버전별 정확한 런타임 라인은 환경에 따라 다를 수 있다.
7. 근본 원인 요약 및 재발 방지
근본 원인 한 줄 정리
panic의 원인 값과 스택트레이스는 별도의 정보 채널이며, 로그에 패닉 위치를 남기려면 recover() 값뿐 아니라 debug.Stack()을 명시적으로 캡처해야 한다.
재발 방지 체크리스트
- 패닉 처리 로직에서
recover()값과debug.Stack()문자열을 함께 로그/리포팅에 저장했는가 -
debug.Stack()호출이 defer 회복 핸들러 안에 있는가 - stdout 리다이렉트에 의존하지 않고 애플리케이션 로그 파이프라인으로 스택을 넘기는가
- 필요 시 전체 goroutine 스택을 살펴보기 위해
runtime.Stack(buf, true)사용 여부를 검토했는가 - 패닉 대신 error value를 반환하는 것(파일·라인은 error wrapping, 예:
fmt.Errorf("%w", err)+ 스택)으로 설계를 바꿀 수는 없는지 판단했는가
DevTrace verdict
이 문제의 핵심은 recover()가 패닉 원인 값만 반환하고 스택 프레임 정보는 별도 채널(runtime/debug.Stack())에 있다는 점이며, 패닉 위치를 정확히 남기려면 반드시 두 채널을 함께 캡처해야 한다.
출처
참고: 본문의 실행 출력(스택트레이스)은 위 StackOverflow 원문에 포함된 Go Playground 검증 결과를 인용한 것이다. go 도구 모음을 로컬에서 실행한 결과는 아니며, 실행 환경에 따라 주소값·라인 번호가 달라질 수 있다. 일반적인 회복/스택 캡처 관행에 대한 언급(“잘못된 해결책” 등)은 범용 지식에 기반한 점검 기준이다.