Go의 비밀스러운 삶: Panic과 Recover

발행: (2026년 2월 10일 오후 02:41 GMT+9)
10 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the article text, I’ll translate it into Korean while preserving all formatting, markdown syntax, and code blocks.

제21장: 비상 브레이크

“내가 죽인 것 같아,” 이든이 속삭였다.

그는 터미널을 바라보고 있었다. 몇 주 동안 매끄럽게 실행되던 그의 웹 서버가 사라졌다. 로그도, 종료 메시지도 없고—그냥 갑자기 명령 프롬프트로 돌아왔다.

“뭐 했어?” 엘리노어가 그의 어깨 너머로 물었다.

“빈 JSON 본문을 가진 POST 요청을 보냈어,” 이든이 말했다. “400 Bad Request 오류가 나올 거라 기대했는데, 대신 서버 전체가 사라졌어.”

“로그 보여줘,” 엘리노어가 말했다.

이든은 스크롤을 올렸다.

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a2b40]

goroutine 1 [running]:
main.HandleRequest(...)
    /server/main.go:42 +0x45
main.main()
    /server/main.go:80 +0x120

“아,” 엘리노어가 고개를 끄덕였다. “nil 포인터의 필드에 접근하려고 했구나. C++에서는 이것이 세그멘테이션 오류가 된다. Go에서는 panic이 발생한다.”

“하지만 나는 어디서든 오류를 체크했어!” 이든이 항변했다.

“panic은 오류가 아니다,” 엘리노어가 바로잡았다. “오류는 문을 두드리는 정중한 알림: ‘죄송합니다, 이 파일을 열 수 없습니다.’ panic은 화재 경보와 같다: ‘모두 불타고 있습니다.’ 즉시 실행을 중단하고, defer된 함수들을 실행한 뒤 프로그램을 크래시한다.”

풀기

“그래서… 게임 오버인가요?” Ethan이 물었다.

“반드시 그렇지는 않아요,” Eleanor가 말했다. “패닉이 발생하면 스택을 따라 함수마다 거품처럼 올라옵니다. 마치 엘리베이터가 샤프트를 자유 낙하하는 것과 같죠.”

그녀는 다이어그램을 그렸다:

HandleRequest panics! → CRASH
ServeHTTP stops      → CRASH
main stops           → EXIT PROGRAM

“긴급 브레이크가 필요해요,” Eleanor가 말했다. “프로세스를 죽이기 전에 자유 낙하를 멈출 메커니즘이 필요합니다.”

Containing the Blast (Recover)

“In Chapter 20 we learned about defer,” she continued. “That is the only place where you can stop a panic.”

“Why only there?”

“Because when a function is panicking it skips all normal code. It only runs deferred functions. If you want to catch the panic, you have to be waiting in the defer stack.”

그녀는 미들웨어를 작성하기 위해 새 파일을 열었다:

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // The safety net caught something!
                log.Printf("Panic recovered: %v", err)
                w.WriteHeader(http.StatusInternalServerError)
            }
        }()

        next.ServeHTTP(w, r)
    })
}

recover() 함수는 마법과도 같아요,” 엘리노어가 설명했다. “프로그램이 패닉 상태이면 recover()가 패닉 값을 잡아내고 충돌하는 엘리베이터를 멈춥니다. 제어권을 당신에게 반환하죠. 프로그램이 패닉 상태가 아니면 nil을 반환하고 아무 일도 하지 않아요.”

“그럼 충돌을 제어된 착륙으로 바꾸는 거군요?”

“맞아요. 폭발 반경을 제한하는 거죠. 하나의 나쁜 요청은 죽지만 서버는 계속 살아남습니다.”

Ethan은 자신의 main 함수를 다음과 같이 업데이트했다:

func main() {
    handler := http.HandlerFunc(HandleRequest)
    safeHandler := RecoveryMiddleware(handler) // Wrap it in the safety net
    http.ListenAndServe(":8080", safeHandler)
}

그는 서버를 실행하고 다시 “독” 요청을 보냈다.

  • Terminal: Panic recovered: runtime error: invalid memory address or nil pointer dereference
  • Server status: Still running.

“크래시가 안 났어요!” Ethan이 외쳤다.

법의 지배

“지금,” 엘리노어가 목소리를 진지하게 낮추며 말했다, “panicrecover를 어떻게 사용하는지 알게 되었겠지. 내 조언은: 하지 말아라.”

Ethan은 눈을 깜빡이며 “뭐라고?”라고 물었다.

“일반적인 오류 처리에 panic을 사용하지 마세요,” 라고 그녀가 경고했다. “사용자 파일이 없으면 error를 반환하고, 데이터베이스가 다운되면 error를 반환하세요.”

“그럼 언제 사용해야 하나요?”

“오직 불가능한 경우에만,” 엘리노어가 말했다. “프로그래머가 너무 심각한 실수를 해서 코드가 논리적으로 계속될 수 없을 때 사용하세요. 혹은 시작 단계에서 필수 설정이 누락된 경우에도 사용합니다.”

func MustLoadConfig() *Config {
    cfg, err := load("config.yaml")
    if err != nil {
        panic("cannot load config: " + err.Error())
        // Acceptable: the app literally cannot start without this.
    }
    return cfg
}

“우리는 시스템의 경계에서 recover를 사용합니다—예를 들어 이 미들웨어처럼—단일 버그 요청이 전체 시스템을 다운시키는 것을 방지하기 위해서죠. 하지만 비즈니스 로직 안에서는? 오류에 집중하세요.”

그녀는 마지막 경고를 덧붙였다. “기억하세요: recover같은 goroutine 안에서만 작동합니다. 백그라운드 워커를 생성하고 그 워커가 panic하면, 이 미들웨어는 당신을 구해주지 못합니다. 프로그램 전체가 크래시됩니다.”

Ethan은 자신의 코드를 보며 말했다. “그러니까… panic은 크래시용이고, error는 문제용이군요.”

“정확히 그렇습니다,” 엘리노어가 미소 지으며 말했다. “그리고 recover는 다음 날에도 디버깅할 수 있게 해주는 낙하산입니다.”

Chapter 21의 핵심 개념

Panic

  • 일반적인 흐름을 멈추게 하는 내장 함수.
  • 효과: 현재 함수를 중단하고, 모든 defer 문을 실행한 뒤 프로그램을 크래시시킴(복구되지 않은 경우).
  • 원인: 런타임 오류(널 포인터 역참조, 인덱스 범위 초과) 또는 명시적인 panic() 호출.

Recover

  • 패닉 중인 goroutine의 제어를 다시 얻는 내장 함수.
  • 조건: deferred 함수 내부에서 호출되어야 함.
  • 동작:
    • 패닉이 발생한 상태에서 호출되면 패닉 값을 잡아내고 크래시를 중단한다.
    • 일반적으로 호출되면 nil을 반환한다.

“Must” 패턴

  • Go에서 초기화 단계에 필수 자원이 실패하면 패닉을 일으키는 관용적인 방식(예: regexp.MustCompile, 설정 로드).
  • 애플리케이션을 실행할 수 없을 경우, 초기에 크래시하도록 설계한다.

Goroutine 경계

  • recover는 패닉이 발생한 같은 goroutine 안에서만 동작한다. 다른 goroutine에서 발생한 패닉은 해당 goroutine 내에서 복구되지 않으면 프로그램을 종료시킨다.

핵심 포인트

  • panic현재 goroutine의 스택을 따라 상승한다.
  • 일반적인 go func() 로 goroutine을 생성하고 그 안에서 패닉이 발생하면, main에서의 recover는 이를 잡지 못한다. 프로그램이 크래시한다.

다음 장: Context 패키지. Ethan은 너무 오래 걸리는 요청을 취소하는 방법을 배운다.

Aaron Rose는 tech-reader.blog의 소프트웨어 엔지니어이자 기술 작가이며, Think Like a Genius의 저자이다.

Back to Blog

관련 글

더 보기 »

Go의 비밀스러운 삶: ‘defer’ 문

챕터 20: The Stacked Deck Ethan의 데스크탑 PC 팬이 크게 돌고 있었다. 그는 오류 메시지를 끊임없이 뿜어내는 터미널을 마치 부서진 불꽃처럼 바라보고 있었다.

Zig vs Go: 제네릭

소개 Go는 버전 1.18에서 generics를 도입하여 함수와 struct를 타입으로 매개변수화할 수 있게 했습니다. Zig는 오랫동안 compile‑time generics를 지원해 왔습니다.