Panic과 Recover: Go의 오류 처리 이해하기
Olivia Novak
Dev Intern · Leapcell

단순성과 명확성을 강조하며 설계된 언어인 Go는 많은 객체 지향 언어와 비교하여 오류 처리에 대한 독특한 접근 방식을 취합니다. Java와 C++는 예외 처리를 위해 try/catch
블록을 사용하는 반면, Go는 의도적으로 이를 생략합니다. 대신 반환 값 기반의 오류 처리 패러다임을 장려합니다. 그러나 Go는 예외적인 상황과 프로그램 종료를 위한 메커니즘, 즉 panic
과 그에 대응하는 recover
를 제공합니다.
panic
과 recover
를 언제 어떻게 사용할지 이해하는 것은 견고하고 관용적인 Go 애플리케이션을 작성하는 데 매우 중요합니다. 이 글에서는 이 두 함수를 자세히 살펴보고 실제 예제와 모범 사례를 논의할 것입니다.
Go의 방식: 오류를 반환 값으로
panic
과 recover
에 대해 자세히 알아보기 전에 Go의 주요 오류 처리 철학을 다시 한번 강조하는 것이 중요합니다. 실패할 수 있는 대부분의 함수는 두 개의 값을 반환합니다. 결과 값과 error
인터페이스입니다. 작업이 성공하면 error
값은 nil
이고, 그렇지 않으면 문제를 설명하는 non-nil 값입니다.
package main import ( "fmt" "strconv" ) func parseAndAdd(str1, str2 string) (int, error) { num1, err := strconv.Atoi(str1) if err != nil { return 0, fmt.Errorf("invalid number 1: %w", err) } num2, err := strconv.Atoi(str2) if err != nil { return 0, fmt.Errorf("invalid number 2: %w", err) } return num1 + num2, nil } func main() { sum, err := parseAndAdd("10", "20") if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Printf("Sum: %d\n", sum) sum, err = parseAndAdd("abc", "20") if err != nil { fmt.Printf("Error: %v\n", err) // Output: Error: invalid number 1: strconv.Atoi: parsing "abc": invalid syntax return } fmt.Printf("Sum: %d\n", sum) }
이 접근 방식은 개발자가 호출 시점에서 명시적으로 오류를 확인하고 처리하도록 장려하여 오류 흐름을 명확하게 하고 추적하기 어려운 "숨겨진" 예외를 최소화합니다.
오류가 패닉이 되는 경우
명시적 오류 반환이 표준이지만, 프로그램이 정상적인 실행 흐름을 계속할 수 없는 상황, 즉 진정으로 복구 불가능한 상태 또는 프로그래머 오류를 나타내는 경우가 있습니다. 이때 panic
이 사용됩니다.
panic
은 내장 함수로, 제어의 일반적인 흐름을 중단하고 패닉을 시작합니다. 함수가 패닉되면 해당 함수의 실행이 중단되고, 지연된 함수들이 실행된 다음, 호출한 함수가 패닉되어 호출 스택을 따라 프로그램이 충돌할 때까지 전파됩니다. 본질적으로 panic
은 무언가가 치명적으로 잘못되었고 프로그램이 진행될 수 없음을 알리는 Go의 방식입니다.
panic
의 일반적인 시나리오:
- 복구 불가능한 런타임 오류: 0으로 나누거나, 범위를 벗어난 슬라이스 인덱스에 접근하거나, 잘못된 타입에 대한 타입 단언을 시도하면 자동으로 패닉이 발생합니다. 이는 대개 코드의 논리적 결함을 나타냅니다.
- 프로그래머 오류: 함수의 불변성을 위반하는 인수를 함수가 받고 계속 실행하면 상태가 손상될 수 있는 경우,
panic
이 적절할 수 있습니다. 예를 들어, 라이브러리 함수가 명시적으로 허용되지 않는nil
포인터로 호출되는 경우입니다. - 초기화 실패: 프로그램이 필수적인 리소스(예: 데이터베이스 연결)를 초기화하지 못하고 이를 통해 절대 작동할 수 없는 경우, 시작 중에 패닉하는 것은 프로그램이 잘못된 상태로 실행되는 것을 방지하는 유효한 전략일 수 있습니다.
init()
함수에서 복구 불가능한 오류가 발생하면 종종 패닉합니다.
panic
의 예:
package main import "fmt" func riskyOperation(index int) { data := []int{1, 2, 3} if index < 0 || index >= len(data) { // 이 함수가 유효한 인덱스를 절대적으로 요구하는 경우 프로그래머 오류 또는 복구 불가능한 상황. panic(fmt.Sprintf("Index out of bounds: %d", index)) } fmt.Printf("Value at index %d: %d\n", index, data[index]) } func main() { fmt.Println("Starting program...") riskyOperation(1) fmt.Println("Operation 1 successful.") // 이것은 패닉을 유발하고 프로그램을 종료시킵니다. riskyOperation(5) fmt.Println("Operation 2 successful.") // 이 줄은 도달되지 않습니다. fmt.Println("Program ended.") }
riskyOperation(5)
가 호출되면 panic
이 발생하고 패닉 메시지가 출력된 후, 프로그램은 후속 fmt.Println
문을 실행하지 않고 종료됩니다.
패닉 잡기: recover
함수
panic
은 일반적으로 복구 불가능한 오류에 사용되지만, Go는 패닉 상태인 고루틴의 제어를 되찾기 위해 recover
를 제공합니다. recover
는 defer
함수 내부에서만 유용한 내장 함수입니다. recover
가 지연된 함수 내에서 호출되고 해당 고루틴이 패닉 중일 때, recover
는 패닉 시퀀스를 중지하고 panic
에 전달된 값을 반환합니다. 고루틴이 패닉 중이 아니면 recover
는 nil
을 반환합니다.
recover
의 주요 사용 사례는 panic
으로부터 정상적으로 정리하고, 프로그램이 종료되기 전에 오류를 기록하거나, 특정 서버 시나리오에서는 단일 문제가 있는 요청이 전체 서버를 중단하는 것을 방지하는 것입니다.
recover
의 예:
package main import "fmt" func protect(f func()) { defer func() { if r := recover(); r != nil { fmt.Printf("Recovered from panic: %v\n", r) } }() f() } func main() { fmt.Println("Main: Starting program.") protect(func() { fmt.Println("Inside protected function 1.") panic("Something went wrong in func 1!") fmt.Println("This will not be printed in func 1.") }) fmt.Println("Main: After protected function 1 call.") // 이 줄은 도달됩니다. protect(func() { fmt.Println("Inside protected function 2.") // No panic here }) fmt.Println("Main: After protected function 2 call.") }
출력:
Main: Starting program.
Inside protected function 1.
Recovered from panic: Something went wrong in func 1!
Main: After protected function 1 call.
Inside protected function 2.
Main: After protected function 2 call.
이 예제에서 protect
함수는 recover
를 호출하는 익명 함수를 사용하는 defer
문을 사용합니다. protect
에 전달된 중첩된 익명 함수가 패닉되면 defer
함수가 실행되고, recover
가 패닉을 잡아서 메시지를 출력합니다. 그런 다음 제어 흐름이 main
으로 돌아가고 프로그램은 충돌하는 대신 계속 실행됩니다.
Panic 대 Error: 중요한 구분
panic
을 사용할 때와 error
를 반환할 때를 구분하는 것이 중요합니다.
- 오류 (반환 값): 예측 가능하지만 바람직하지는 않은 상황으로, 호출자가 정상적으로 처리할 수 있습니다. 여기에는 잘 설계된 애플리케이션의 대부분의 오류 조건(예: 파일을 찾을 수 없음, 잘못된 입력, 네트워크 시간 초과)이 포함됩니다.
- 패닉: 복구 불가능한 프로그래머 오류 또는 프로그램이 합리적으로 계속할 수 없는 상태를 나타내는 진정으로 예외적인 상황을 위한 것입니다.
recover
메커니즘이 서버의 복원력을 관리하기 위해 명시적으로 설정되지 않는 한(예: 개별 요청 처리기에서 패닉을 잡아 서버를 계속 실행 상태로 유지), 패닉은 일반적으로 프로그램 종료로 이어져야 합니다.
다음과 같은 사고 모델을 고려하십시오:
- 외부 사용자 입력이나 환경적 요인이 문제로 이어지면 오류일 가능성이 높습니다.
- 문제의 원인이 코드 내의 잘못된 논리이고 방지되었어야 하는 경우, 종종 panic이 적합합니다.
모범 사례 및 관용구
-
일반적인 오류 처리에
panic
을 사용하지 마십시오: 이것이 황금률입니다.panic
은 Go의try-catch
에 해당하는 것이 아닙니다.panic
을 과도하게 사용하면 코드를 이해하고 디버깅하며 추론하기가 더 어려워집니다. 명시적인 오류 확인 흐름을 우회하기 때문입니다. -
복구 불가능한 상황에
panic
을 사용하십시오: 라이브러리 불변성이 위반되거나 애플리케이션의 중요한 부분이 초기화되지 못하면panic
이 적합합니다. -
recover
는 주로 서버 복원력 / 최상위 오류 로깅에 사용됩니다: 웹 서버 또는 장기간 실행되는 데몬에서는panic
이 전체 서버를 충돌시키지 않도록 개별 요청 처리기 주변에서recover
가 종종 사용됩니다. 이를 통해 서버는 패닉을 기록하고 클라이언트에 내부 서버 오류를 반환하며 다른 요청을 계속 처리할 수 있습니다.package main import ( "fmt" "net/http" "runtime/debug" // 스택 추적용 ) func myHandler(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { fmt.Printf("Recovered from panic in handler: %v\n", r) debug.PrintStack() // 디버깅을 위해 스택 추적 출력 http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() // 예상치 못한 조건으로 인한 패닉 시뮬레이션 if r.URL.Path == "/panic" { panic("Simulated unhandled error for path /panic") } fmt.Fprintf(w, "Hello, Go user! Path: %s\n", r.URL.Path) } func main() { http.HandleFunc("/", myHandler) fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) }
이 웹 서버 예제에서
/panic
에 대한 요청이 발생하면myHandler
가 패닉하지만,recover
가 있는defer
는 이를 잡아 오류(스택 추적 포함)를 기록하고 500 응답을 보내 서버가 충돌하는 것을 방지합니다. -
스택 추적 기록 고려: 특히 서버 환경에서 패닉을 복구할 때
debug.PrintStack()
또는 유사한 도구를 사용하여 스택 추적을 기록하는 것이 종종 유익합니다. 이는 패닉의 근본 원인을 디버깅하는 데 중요한 정보를 제공합니다. -
기록/정리 후 재패닉 (선택 사항): 때로는 정리를 수행하거나 기록한 후, 특정 작업에 대해 해당 문제가 여전히 근본적으로 복구 불가능한 경우 다시 패닉하기를 원할 수 있습니다. 이는
defer
블록 내에서panic(r)
를 다시 호출하여 수행할 수 있습니다.defer func() { if r := recover(); r != nil { fmt.Printf("Caught panic: %v. Performing cleanup...\n", r) // ... 정리 수행 ... panic(r) // 스택을 계속 해제하기 위해 다시 패닉 } }()
결론
명시적인 error
반환 값에 초점을 맞춘 Go의 오류 처리는 명확성과 견고성을 촉진합니다. panic
과 recover
는 진정으로 예외적이고 일반적으로 복구 불가능한 상황이나 프로그래머 오류를 처리하는 별도의 전문적인 역할을 수행합니다. panic
은 종료로 이어지는 심각한 문제를 신호하지만, recover
는 정상적인 종료 또는 서버 가동 시간을 유지하기 위한 안전망을 제공합니다. 이러한 메커니즘의 적절한 사용을 마스터하는 것이 언어 설계 철학을 진정으로 구현하는 관용적이고 안정적이며 유지 관리 가능한 Go 애플리케이션을 작성하는 데 핵심입니다.