Golang에서 log.Fatal() vs panic() 차이점에 대해 알아보기

Golang에서 log.Fatal() vs panic() 차이점에 대해 알아보기

2025년 2월 11일

“이런 경우 log.Fatal()보다 panic()을 사용하는게 더 좋은 것 같아요”

최근 log.Fatal()을 사용하다 위와 같은 피드백을 들었다.

음? log.Fatal()log를 좀 더 잘 찍어주는거 아닌가? 하고 생각했다

부끄러운 사실이지만, 최근이 되어서야 Golang에서 log.Fatal()panic()의 차이점을 명확하게 알게 되어 이번 기회에 정리해보려고 한다.

log.Fatal()과 panic()의 차이점

log.Fatal()panic()은 둘 다 프로그램을 종료시키는 함수이다. 코드로 동작을 살펴보자

package main

import (
    "log"
	"log/slog"
)

func RunWithFatal() {
	log.Fatal("This is a fatal error")
}

func main() {
	RunWithFatal()

	slog.Info("This is not executed")
}

위 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

2025/02/11 20:02:31 This is a fatal error

이제 panic()을 사용한 코드를 살펴보자.

package main

import (
    "log/slog"
)

func RunWithPanic() {
    panic("This is a panic error")
}

func main() {
    RunWithPanic()

    slog.Info("This is not executed")
}

위 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

panic: This is a panic error

goroutine 1 [running]:
main.RunWithPanic(...)
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:8
main.main()
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:12 +0x30

위 코드를 보면 log.Fatal()은 에러를 출력하고 프로그램을 종료시키지만, panic()은 에러를 출력하고 프로그램을 종료시키는 것은 동일하지만, panic()은 스택 트레이스를 출력한다.

recover()를 사용하여 복구하기

panic()을 사용하면 프로그램이 종료되지만, recover()를 사용하면 프로그램을 종료하지 않고 복구할 수 있다.

사실상 panic이 아예 없다면 좋겠지만, 개발자도 사람이기에 실수를 할 수 밖에 없다. 그렇기에 일반적으로 API 서버와 같은 곳에서는 panic()recover()하는 미들웨어를 만들어 복구하는 방식으로 서버가 의도치 않게 죽어버리는 상황을 방지하곤 한다.

차이를 명확하게 알기 위해 먼저 log.Fatal()을 recover()를 해보자

package main

import (
    "log"
    "log/slog"
)

func RunWithFatal() {
    log.Fatal("This is a fatal error")
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            slog.Info("Recovered from", "error", r)
        }
    }()

    RunWithFatal()

    slog.Info("This is not executed")
}

위 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

2025/02/11 20:07:49 This is a fatal error

복구되지 않은 것이다. 그렇다면 panic()recover()를 해보자

package main

import (
	"log/slog"
	"runtime/debug"
)

func RunWithPanic() {
	panic("This is a panic error")
}

func main() {
	defer func() {
		if r := recover(); r != nil {
			slog.Info("Recovered from", "error", r)
			debug.PrintStack()
		}
	}()

	RunWithPanic()
}

위 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

2025/02/11 20:09:51 INFO Recovered from error="This is a panic error"
goroutine 1 [running]:
runtime/debug.Stack()
	/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:26 +0x64
runtime/debug.PrintStack()
	/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:18 +0x1c
main.main.func1()
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:16 +0x8c
panic({0x1004d6560?, 0x1004f4190?})
	/opt/homebrew/opt/go/libexec/src/runtime/panic.go:785 +0x124
main.RunWithPanic(...)
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:9
main.main()
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:20 +0x4c

사용하지 않아도 되지만, 좀전과 같은 스택 트레이스를 출력하기 위해 debug.PrintStack()를 사용하였다.

일반적으로 미들웨어에서는 심각도가 높은 panic과 같은 상황에서 개발자가 빨리 알아차릴 수 있도록 위와 같은 스택트레이스를 포함한 로그를 많이 남긴다.

debug.Stack() 메서드를 통해 Stderr에 출력하지 않고도, 스택 트레이스를 직접 핸들링할 수도 있다.

용도

log.Fatal()은 내부적으로 os.Exit(1)을 호출한다.

애초에 에러코드와 함께 프로그램을 즉시 종료하기 위해 만들어졌고, 그렇기에 recover()로 복구할 수 없다.

반면 panic()recover()로 복구할 수 있다.

발생해선 안되지만, 발생했을 때 복구할 수 있는 상황이라면 panic()을 사용하는 것이 더 좋다.

일반적으로 라이브러리 함수나, 특정 패키지의 함수의 경우에 panic()을 사용하는 것이 좋다. (라이브러리 때문에 서버가 죽었는데 복구도 되지 않는다면 그 결과는 참혹할 것이다.)

log.Fatal()의 경우에는 main() 함수와 같은 경우에 error를 최종적으로 핸들링할 때 사용하는 것이 좋다.

예를 들어 의존성을 불러오는 과정에서 프로그램을 실행시킬 수 없을만한 에러가 발생했을 때 해당 의존성 초기화 하는 모듈은 error를 반환하고, main() 함수에서 log.Fatal()을 호출하는 식이다.

구조를 본다면 이럴 것이다.

단순하게 적은 것이다. 참고용으로만 보자.

package main

type Dependencies struct {
	DB *sql.DB
	redis *redis.Client
	...
}

func NewDependencies() (*Dependencies, error) {
    db, err := sql.Open("mysql", "user:password@/dbname")
    if err != nil {
        return nil, err
    }

    redis := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
	
    if err = redis.Ping().Err(); err != nil {
        return nil, err
    }

    return &Dependencies{
        DB: db,
        redis: redis,
    }, nil
    
}


func main() {
    deps, err := NewDependencies()
    if err != nil {
        log.Fatal(err)
    }
	
	// ...
}

+ log.Panic()

log 패키지에는 log.Panic()이라는 함수도 있다.

panic()에 로그 기능을 더한 것으로 아래와 같은 코드를 실행시키면

package main

import (
	"log"
	"log/slog"
	"runtime/debug"
)

func RunWithPanic() {
	log.Panic("This is a panic error")
}

func main() {
	defer func() {
		if r := recover(); r != nil {
			slog.Info("Recovered from", "error", r)
			debug.PrintStack()
		}
	}()

	RunWithPanic()
}

다음과 같이 출력된다.

2025/02/11 20:23:17 This is a panic error
2025/02/11 20:23:17 INFO Recovered from error="This is a panic error"
goroutine 1 [running]:
runtime/debug.Stack()
	/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:26 +0x64
runtime/debug.PrintStack()
	/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:18 +0x1c
main.main.func1()
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:17 +0x8c
panic({0x100ad24e0?, 0x140000100a0?})
	/opt/homebrew/opt/go/libexec/src/runtime/panic.go:785 +0x124
log.Panic({0x1400010af20?, 0x0?, 0x68?})
	/opt/homebrew/opt/go/libexec/src/log/log.go:432 +0x60
main.RunWithPanic(...)
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:10
main.main()
	/Users/code_kirin/dev/personal/awesomeProject6/main.go:21 +0x60

panic과 비교했을 때, 로그 기능을 더한 것이라 보면 된다. 동일하게 panic()을 발생시키지만, 로그를 남기는 것이 특징이라고 볼 수 있다.

Reference