아쉬운 일이지만, Go 언어로 API 서버를 만들다 보면 어쩔 수 없이 Panic이 발생하는 경우가 있다.
예를 들어 대표적으로는 다음과 같다.
- nil 포인터의 리시버 메서드 사용
- 배열의 존재하지 않는 인덱스 접근
- 타입 어설션 실패
- map의 동시 쓰기
- 닫힌 채널에 쓰기
이런 경우에는 표준 라이브러리 net/http
로 구성된 경우 서버가 Panic을 핸들링한다.
예를 들어, 다음과 같이 코드를 작성하면 서버는 Panic을 기본적으로 Recover하고 빈 응답을 반환한다.
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "Some logic 1 executed")
slog.InfoContext(r.Context(), "Some logic 2 executed")
slog.InfoContext(r.Context(), "Panic occurred")
panic("Something went wrong")
w.Write([]byte("Hello, World!"))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", SomeHandler())
port := ":8080"
slog.Info("Starting server", "port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
2025/03/19 12:10:49 INFO Some logic 1 executed
2025/03/19 12:10:49 INFO Some logic 2 executed
2025/03/19 12:10:49 INFO Panic occurred
2025/03/19 12:10:49 http: panic serving [::1]:58424: Something went wrong
goroutine 78 [running]:
net/http.(*conn).serve.func1()
/opt/homebrew/opt/go/libexec/src/net/http/server.go:1947 +0xb0
panic({0x102508980?, 0x10256dd90?})
/opt/homebrew/opt/go/libexec/src/runtime/panic.go:792 +0x124
main.main.SomeHandler.func1({0x14000108ae8?, 0x1022756a0?}, 0x1400017af00)
/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:44 +0x9c
net/http.HandlerFunc.ServeHTTP(0x1400013a0c0?, {0x1025720b8?, 0x1400016cc40?}, 0x102417484?)
/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38
net/http.(*ServeMux).ServeHTTP(0x0?, {0x1025720b8, 0x1400016cc40}, 0x1400017af00)
/opt/homebrew/opt/go/libexec/src/net/http/server.go:2822 +0x1b4
net/http.serverHandler.ServeHTTP({0x140001f4420?}, {0x1025720b8?, 0x1400016cc40?}, 0x1?)
/opt/homebrew/opt/go/libexec/src/net/http/server.go:3301 +0xbc
net/http.(*conn).serve(0x14000147290, {0x1025724c8, 0x14000117080})
/opt/homebrew/opt/go/libexec/src/net/http/server.go:2102 +0x52c
created by net/http.(*Server).Serve in goroutine 1
/opt/homebrew/opt/go/libexec/src/net/http/server.go:3454 +0x3d8
다른 언어와 프레임워크는 어떤가?
다른 언어도 마찬가지이다. 예를 들어 null pointer exception과 같은 오류가 발생하면, 적절한 예외 처리가 없을 경우 프로그램은 어쩔 수 없이 종료되지만, Spring과 같은 많은 현대 프레임워크의 경우에는 이러한 에러들을 프레임워크 단에서 처리하여 서버가 종료되지 않도록 한다.
이대로 두어도 괜찮은가?
기본 동작도 훌륭한 편이지만, 프론트엔드 개발자나 사용자에게 단순히 빈 응답을 보내는 것은 꽤 무례한 대응으로 볼 수 있다. 에러가 발생했다면, 어떤 에러가 발생했는지 명시하고 그에 맞는 에러 응답을 보내는 것이 일반적이고 상식적인 방법이다.
예를 들어, 500 INTERNAL_SERVER_ERROR
의 경우 다음과 같은 응답을 보내는 것이다.
{
"error": "Internal Server Error",
"message": "Something went wrong"
}
이런 응답을 하지 않은 표준 라이브러리인 net/http
가 문제라고 할 수도 있겠지만, Server의 사용 환경이 매우 다양하기에 가장 일반적이고 보수적인 방법을 선택한 것이라고 생각한다.
이런 문제를 해결하기 위해 Go에서는 미들웨어를 사용할 수 있다.
Middleware란?
이전에 다루었던 Go API 서버에서 Request ID를 로깅하는 방법에서 간단히 언급했듯이, 미들웨어는 요청과 응답 사이에서 동작하는 코드를 의미한다.
일반적으로 로깅, 인증, 예외 처리와 같이 API에서 공통적으로 수행해야 하는 부분을 미들웨어로 구현하여 사용한다.
미들웨어가 다른 미들웨어를 반환하는 방식으로 구성하면, 미들웨어 간 체인을 형성하여 API는 비즈니스 로직에만 집중할 수 있다.
이를 다이어그램으로 나타내면 다음과 같다.
flowchart LR subgraph Middleware1 subgraph Middleware2 subgraph Middleware...N API end end end client1[Client] -->|Request| Middleware1 Middleware1 -->|Response| client2[Client]
구현
Go에서 일반적인 미들웨어는 다음과 같이 구현할 수 있다.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do something before
next.ServeHTTP(w, r)
// Do something after
})
}
여기서 next
파라미터는 다음 미들웨어나 실제 API 로직을 담고 있는 Handler
를 가리킨다.
실제 호출 시에는 다음과 같이 사용할 수 있다.
func main() {
http.Handle("/", Middleware(http.HandlerFunc(Handler)))
http.ListenAndServe(":8080", nil)
}
미들웨어를 여러 개 묶는다면 다음과 같이 사용할 수 있다.
http.Handle("/", Middleware1(Middleware2(http.HandlerFunc(Handler))))
Panic Recover Middleware
이제 Panic을 Recover하는 미들웨어를 구현해보자.
요구 사항에 따라 다르겠지만, 필자의 경우에는 slog 패키지를 이용해 로그를 남기고 500 Internal Server Error를 반환하도록 간단하게 구현하였다.
func PanicMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
// http.ErrAbortHandler는 Recover하지 않는다.
// 이 경우 클라이언트에 응답을 보내지 않으며 로그에도 기록하지 않는다.
panic(r)
}
slog.Error("HTTP handler panicked", "err", r, "stacktrace", string(debug.Stack()))
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
slog.Info("Panic Recovered")
return
}
}()
next.ServeHTTP(w, r)
})
}
r := recover()
를 통해 Panic이 발생하면, r
에 Panic의 내용이 담기게 된다.
이를 slog 패키지를 이용해 로그로 남기고, 500 Internal Server Error를 반환하도록 처리하였다.
특히, http.ErrAbortHandler
는 Recover하지 않도록 하였다.
이는 http.Error
등에서 사용되는 특수한 에러로, 이 에러가 발생하면 클라이언트에 응답을 보내지 않고 스택 트레이스 로그도 남기지 않는다.
해당 에러에 대한 설명은 아래와 같이 적혀 있다.
// ErrAbortHandler is a sentinel panic value to abort a handler.
// While any panic from ServeHTTP aborts the response to the client,
// panicking with ErrAbortHandler also suppresses logging of a stack
// trace to the server's error log.
var ErrAbortHandler = errors.New("net/http: abort Handler")
결과
이제 미들웨어를 테스트해보자.
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "Some logic 1 executed")
slog.InfoContext(r.Context(), "Some logic 2 executed")
slog.InfoContext(r.Context(), "Panic occurred")
panic("Something went wrong")
w.Write([]byte("Hello, World!"))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", PanicMiddleware(SomeHandler()))
port := ":8080"
slog.Info("Starting server", "port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
핸들러와 메인 코드를 위와 같이 작성하고 http://localhost:8080으로 접속하면 다음과 같은 결과를 볼 수 있다.
2025/03/19 11:33:31 INFO Starting server port=:8080
2025/03/19 11:33:39 INFO Some logic 1 executed
2025/03/19 11:33:39 INFO Some logic 2 executed
2025/03/19 11:33:39 INFO Panic occurred
2025/03/19 11:33:39 ERROR HTTP handler panicked err="Something went wrong" stacktrace="goroutine 35 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:26 +0x64\nmain.main.PanicMiddleware.func2.1()\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:19 +0x70\npanic({0x1004809c0?, 0x1004e5e70?})\n\t/opt/homebrew/opt/go/libexec/src/runtime/panic.go:792 +0x124\nmain.main.SomeHandler.func1({0x1400000e0e0?, 0x1400000e0d4?}, 0x1400007e000)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:44 +0x9c\nnet/http.HandlerFunc.ServeHTTP(0x100b1cce8?, {0x1004ea198?, 0x14000198000?}, 0x10020184c?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nmain.main.PanicMiddleware.func2({0x1004ea198?, 0x14000198000?}, 0x14000163b28?)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:32 +0x6c\nnet/http.HandlerFunc.ServeHTTP(0x1400013a0c0?, {0x1004ea198?, 0x14000198000?}, 0x10038f5a4?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nnet/http.(*ServeMux).ServeHTTP(0x14000182000?, {0x1004ea198, 0x14000198000}, 0x1400007e000)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2822 +0x1b4\nnet/http.serverHandler.ServeHTTP({0x14000072030?}, {0x1004ea198?, 0x14000198000?}, 0x6?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3301 +0xbc\nnet/http.(*conn).serve(0x140001462d0, {0x1004ea5a8, 0x14000115080})\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2102 +0x52c\ncreated by net/http.(*Server).Serve in goroutine 1\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3454 +0x3d8\n"
2025/03/19 11:33:39 INFO Panic Recovered
2025/03/19 11:33:39 INFO Some logic 1 executed
2025/03/19 11:33:39 INFO Some logic 2 executed
2025/03/19 11:33:39 INFO Panic occurred
2025/03/19 11:33:39 ERROR HTTP handler panicked err="Something went wrong" stacktrace="goroutine 35 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:26 +0x64\nmain.main.PanicMiddleware.func2.1()\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:19 +0x70\npanic({0x1004809c0?, 0x1004e5e70?})\n\t/opt/homebrew/opt/go/libexec/src/runtime/panic.go:792 +0x124\nmain.main.SomeHandler.func1({0x1400000e1b0?, 0x140000140e4?}, 0x1400007e140)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:44 +0x9c\nnet/http.HandlerFunc.ServeHTTP(0x100b1cce8?, {0x1004ea198?, 0x140001980e0?}, 0x10020184c?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nmain.main.PanicMiddleware.func2({0x1004ea198?, 0x140001980e0?}, 0x14000163b28?)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:32 +0x6c\nnet/http.HandlerFunc.ServeHTTP(0x1400013a0c0?, {0x1004ea198?, 0x140001980e0?}, 0x10038f5a4?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nnet/http.(*ServeMux).ServeHTTP(0x140001820a0?, {0x1004ea198, 0x140001980e0}, 0x1400007e140)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2822 +0x1b4\nnet/http.serverHandler.ServeHTTP({0x14000072030?}, {0x1004ea198?, 0x140001980e0?}, 0x6?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3301 +0xbc\nnet/http.(*conn).serve(0x140001462d0, {0x1004ea5a8, 0x14000115080})\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2102 +0x52c\ncreated by net/http.(*Server).Serve in goroutine 1\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3454 +0x3d8\n"
2025/03/19 11:33:39 INFO Panic Recovered
panic 값에 http.ErrAbortHandler
를 넣어보면 다음과 같이 동작한다.
panic(http.ErrAbortHandler)
2025/03/19 11:43:17 INFO Some logic 1 executed
2025/03/19 11:43:17 INFO Some logic 2 executed
2025/03/19 11:43:17 INFO Panic occurred
panic을 핸들링하지 않아 스택도 출력되지 않고 로그도 남지 않는다.
다만, net/http
에서는 서버가 종료되지 않도록 기본적인 Recover는 수행하는 것을 볼 수 있다.