Go panic, recover – 업무에 사용하는 Go 언어 16

go panic, recover
go panic, recover

Go 언어를 사용하면서 흔히 접할 수 있는 개념 중 하나가 바로 panic입니다.
다른 언어에서의 예외(Exception)처럼 느껴질 수도 있지만, Go에서는 panicrecover라는 독특한 방식으로 예외 상황을 처리합니다.
이번 글에서는 go panic의 개념, 사용 방법, 예제 코드, 그리고 주의할 점까지 자세히 살펴보겠습니다.


go panic

Go에서 panic은 프로그램 실행 도중 치명적인 오류가 발생했을 때 사용됩니다.
절대로 일어나면 안 되는 상황에 발생합니다.
panic이 호출되면 현재 함수의 실행이 중지되고, defer 함수들이 실행된 후 런타임 에러 메시지와 함께 프로그램이 종료됩니다.

Go
func main() {
    var names = []string{"Alice", "Bob"}
    fmt.Println(names[2]) // 존재하지 않는 인덱스 접근으로 panic 발생 !!!
}

위 코드에서는 names[2]가 존재하지 않기 때문에 런타임 에러와 함께 panic이 발생하게 됩니다.

필자의 경험상, 초기에 Go를 접했을 때는 이런 오류 메시지가 매우 당황스럽게 느껴졌다.
하지만 panic은 프로그램이 완전히 멈춰야 할 정도의 중대한 문제를 알려주는 수단이기 때문에,
오히려 error와는 구분될 수 있도록 문제가 빨리 드러나서 디버깅에 도움이 되기도 했다.

go panic의 직접 호출

Go에서는 개발자가 직접 panic() 함수를 호출할 수도 있습니다.
이 기능은 프로그램이 더 이상 정상적인 상태로 실행될 수 없다고 판단되는 극단적인 상황에서 사용됩니다.
예를 들어, 중요한 설정 값이 누락되었거나 시스템이 필수 리소스를 사용할 수 없는 경우가 그 예입니다.

필자의 경우, .env 파일이 누락되었거나 db 테이블 설정에 실패한 경우에는 단호하게 panic을 사용하는 편이다.
이렇게 하면 문제 상황을 조기에 포착하고 원인을 빠르게 추적할 수 있다.
하지만, 무분별하게 프로그램이 멈추지는 않도록 다른 일반적인 error에 대해서는 모두 값으로 처리하는 것을 추천한다.

error에 대해서 값으로 처리한다는 개념이 무엇인지 모르겠다면 이전 글을 꼭 보고 오도록 하자.

다음은 panic을 직접 호출하는 필자의 실제 프로젝트 코드를 참고하여 만들어 본 실용적인 예시입니다.

Go
func loadConfig(path string) string {
    if path == "" {
        panic("설정 파일 경로가 제공되지 않았습니다")
    }
    // 실제로는 파일을 읽고 처리하는 로직이 들어감
    return "config loaded"
}

func main() {
    config := loadConfig("")
    fmt.Println(config)
}

위 코드는 loadConfig 함수에 잘못된 입력이 들어올 경우, 즉 설정 파일 경로가 비어 있으면 즉시 panic을 일으켜 프로그램을 중단시킵니다.
이런 방식이 문제를 숨기지 않고 즉시 드러내는 점에서 유지보수 측면에서 오히려 긍정적인 효과를 가져온다고 생각합니다.
단, 이러한 방식은 초기화 로직에서 한정적으로 사용하는 것이 바람직하며, 일반적인 로직에서는 가급적 error를 반환하고 처리하는 구조가 더 좋습니다.

Go
func divide(a, b int) int {
    if b == 0 {
        panic("0으로 나눌 수 없습니다")
    }
    return a / b
}

func main() {
    fmt.Println(divide(10, 0))
}

이 코드에서 b가 0일 경우 panic을 수동으로 발생시킵니다.
특정 조건에서 프로그램이 절대 계속 실행되어선 안 된다고 판단될 경우,
로그만 남기고 단호히 panic을 사용하는 것이 유리한 경우가 있을 수 있습니다.
다만, 이는 신중하게 결정해야 합니다.


recover로 panic 복구하기

Go는 panic을 일으키는 대신 recover를 통해 프로그램의 흐름을 다시 정상으로 되돌릴 수 있는 기능도 제공합니다.
단, recover는 반드시 defer 함수 안에서 호출되어야 작동합니다.

Go
func safeDivide(a, b int) int {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("복구됨 - 에러 내용:", r)
        }
    }()

    return a / b
}

func main() {
    fmt.Println("결과:", safeDivide(10, 0))
    fmt.Println("프로그램이 정상적으로 계속 실행됩니다.")
}

이 예제에서는 10 / 0 연산이 panic을 유발하지만, recover를 통해 복구하여 프로그램이 종료되지 않고 다음 코드로 이어질 수 있습니다.
테스트 환경이나 외부 시스템과의 통신처럼 예기치 못한 에러가 날 가능성이 높은 영역에서 이와 같은 구조를 가끔 사용합니다.
(사실 이런 부분도 왠만하면 error로 처리 가능합니다.)


panic의 사용은 신중해야 합니다

Go는 에러 처리를 명시적으로 하는 철학을 가지고 있기 때문에, panic은 정말 최악의 상황에서만 사용하는 것이 바람직합니다.
일반적인 에러는 error 타입을 통해 처리하는 것이 Go 스타일에 더 맞습니다.
무분별하게 panic을 사용하게 되면 코드 흐름이 복잡해지고, 예측 불가능한 버그를 유발할 수 있습니다.

예를 들어 다음과 같이 panic을 과도하게 사용하는 것은 좋지 않습니다:

Go
func getItem(index int, items []string) string {
    if index < 0 || index >= len(items) {
        panic("인덱스 범위를 벗어났습니다")
    }
    return items[index]
}

이보다는 에러를 반환하고 호출자에게 값으로 처리를 맡기는 방식이 더 적절합니다:

Go
func getItem(index int, items []string) (string, error) {
    if index < 0 || index >= len(items) {
        return "", fmt.Errorf("인덱스 %d는 유효하지 않습니다", index)
    }
    return items[index], nil
}

Go 언어의 panic은 강력한 기능이지만 동시에 위험한 기능입니다.
프로그램의 중단을 수반하기 때문에, 가능한 한 일반적인 error를 통한 처리 방식이 우선시되어야 합니다.
필자는 panic을 디버깅 도구처럼 임시로 사용하는 경우도 있었지만, 실제 운영 코드에는 거의 사용하지 않습니다.