> ## Content Index
> Fetch the complete content index at: https://www.popit.kr/llms.txt
> Use this file to discover other available public pages before exploring further.

# Golang Error Stack Trace와 로깅
- URL: https://www.popit.kr/golang-error-stack-trace-logging/
- Published: 2022-03-27T06:59:49.000Z
- Updated: 2022-03-27T06:59:49.000Z
- Author: 유 영모
- Tags: #소프트웨어, golang, error, Go, echo, stack trace, 로깅

나는 최근 몇 년 간 Golang(이하 Go)을 사용하고 있지만 이전에는 주로 Java로 개발했었다. Java는 오류를 Exception으로 다루며 오류가 발생하면 개발자가 별도 처리하지 않아도 Strack Trace를 출력한다.

아래 그림을 보자. C의 23줄에서 오류가 발생한 상황이다.

![Untitled](https://www.popit.kr/content/images/wp/2022/03/Untitled-600x233.png)

Stack Trace로 표현하면 아래와 같다. 오류와 어떤 경로로 발생했는지 보여주는 것이다.

- C의 C-1 함수 23 줄에서 오류 발생
  - B의 B-1 함수가 C의 C-1 함수 호출
    - A가 B의 B-1 함수 호출

Go에서는 오류를 Error로 다룬다. 아쉽게도 **Error에는 기본적으로 Stack Trace가 포함되어 있지 않아 오류가 발생 시 Stack Trace 없이 오류 메시지만 출력한다.**

아래 코드를 보자.  

```go
package main

import (
  "errors"
  "fmt"
)

func main() {
  player := Player{Name: "유영모"}

  err := player.Shoot()
  if err != nil {
    fmt.Println(err)
  }
}

type Player struct {
  Name string
}

func (p Player) Shoot() error {
  return errors.New("오류 발생!!")
}
```

코드를 실행해 보면 오류 메시지만 출력한 것을 확인할 수 있다.  

```
오류 발생!!
```

간단한 코드에서는 문제없지만 코드 규모가 커지고 복잡해지면 Stack Trace 없이 오류 메시지만으로 문제를 빨리 찾아 해결하기 어렵다.

이 글은 Go에서 Error Stack Trace를 출력하는 방법을 소개하며 이에 더해 Error를 효과적으로 로깅(Logging)하는 방법을 다룬다.

# [github.com/pkg/errors](https://github.com/pkg/errors?ref=popit.kr)

Stack Trace를 출력하는 방법은 Go가 기본적으로 제공하는 errors 패키지 대신 [github.com/pkg/errors](https://github.com/pkg/errors?ref=popit.kr) 를 사용하는 것이다.

먼저 아래 명령어로 모듈을 설치한다.  

```
go get github.com/pkg/errors
```

그리고 앞서 코드에서 errors 패키지를 대체하고 에러 출력시 %+v 문자열 포멧팅으로 변경하면 끝이다.  

```go
package main

import (
  "fmt"
  "github.com/pkg/errors" // 1. errors 패키지 대체
)

func main() {
  player := Player{Name: "유영모"}

  err := player.Shoot()
  if err != nil {
    fmt.Printf("%+v", err) // 2. 출력시 문자열 포멧팅(%+v) 사용
  }
}

type Player struct {
  Name string
}

func (p Player) Shoot() error {
  return errors.New("오류 발생!!")
}
```

에러 출력 시 사용한 문자열 포멧팅 `%+v` 의 의미는 값이 구조체인 경우 구조체의 필드명까지 출력하라는 의미다.

코드를 실행해 보면 오류 메시지와 함께 Stack Trace가 출력되는 것을 확인할 수 있다.  

```
오류 발생!!
main.Player.Shoot
        /Users/yooyoungmo/go/src/stack-trace/main.go:23
main.main
        /Users/yooyoungmo/go/src/stack-trace/main.go:12
runtime.main
        /usr/local/go/src/runtime/proc.go:250
runtime.goexit
        /usr/local/go/src/runtime/asm_arm64.s:1259
```

# errors.Wrap

내가 만든 코드에서는 [github.com/pkg/errors](http://github.com/pkg/errors?ref=popit.kr) 패키지로 Stack Trace를 출력할 수 있다. 하지만 내가 만들지 않고 사용만 하는 코드(예.Go SDK나 github에서 받은 모듈) 에서 반환받은 error는 어떻게 처리해야 할까?

앞선 예제를 조금 수정해 보자.  

```go
package main

import (
  "encoding/json"
  "fmt"
)

func main() {
  player := Player{Name: "유영모"}

  err := player.Shoot()
  if err != nil {
    fmt.Printf("%+v", err)
  }
}

type Player struct {
  Name string
}

func (p Player) Shoot() error {
  var s = `{"name:gopher","age"7}` // 유효하지 않는 JSON 문자열
  var result = map[string]interface{}{}

  err := json.Unmarshal([]byte(s), &result)
  if err != nil {
    return err
  }

  return nil
}
```

Shoot 함수는 일부러 유효하지 JSON 문자열을 `encoding/json` 패키지의 json 모듈로 `Unmarshal` 해서 오류를 발생시켰다.

코드를 실행하면 Stack Trace 없이 오류 메시지만 출력하는 것을 확인할 수 있다.  

```
invalid character ',' after object key
```

이때 사용할 수 있는 것이 errors.Wrap 이다. 함수 이름에서 알 수 있듯이 감싸는 것이다. Unmarshal 함수에서 반환한 error를 errors.Wrap 으로 감싸서 반환한다.  

```go
func (p Player) Shoot() error {
  var s = `{"name:gopher","age"7}`
  var result = map[string]interface{}{}

  err := json.Unmarshal([]byte(s), &result)
  if err != nil {
    return errors.Wrap(err, "JSON 오류 발생!") // <-- 감싸서 반환
  }

  return nil
}
```

코드는 실행해 보면 Stack Trace와 오류 메시지가 함께 출력되는 것을 확인할 수 있다.  

```
invalid character ',' after object key
JSON 오류 발생!
main.Player.Shoot
        /Users/yooyoungmo/go/src/stack-trace/main.go:28
main.main
        /Users/yooyoungmo/go/src/stack-trace/main.go:12
runtime.main
        /usr/local/go/src/runtime/proc.go:250
runtime.goexit
        /usr/local/go/src/runtime/asm_arm64.s:1259
```

# Echo 웹 프레임워크에서 효과적으로 Error 로깅하기

[Echo 웹 프레임워크](https://echo.labstack.com/?ref=popit.kr)(이하 Echo)에서 Error가 발생하면 자동으로 HTTP Status Code를 500(Internal Server Error)을 반환하고 Stack Trace를 출력하게 만들어 보자.

먼저 Echo의 [기본 에러 핸들러(Default HTTP Error Handler)](https://echo.labstack.com/guide/error-handling/?ref=popit.kr#default-http-error-handler)를 대체하는 [커스텀 에러 핸들러(Custom HTTP Error Handler)](https://echo.labstack.com/guide/error-handling/?ref=popit.kr#custom-http-error-handler)를 만든다.  

```go
func customHTTPErrorHandler(err error, c echo.Context) {
  var (
    code = http.StatusInternalServerError
    msg  interface{}
  )

  if he, ok := err.(*echo.HTTPError); ok {
    code = he.Code
    msg = he.Message
  } else {
    msg = http.StatusText(code)
  }

  if _, ok := msg.(string); ok {
    msg = map[string]interface{}{"message": msg}
  }

  if code == http.StatusInternalServerError {
    log.Errorf("%+v", err)
    c.String(code, "Internal Server Error")
  } else {
    c.JSON(code, msg)
  }
}
```

커스텀 에러 핸들러는 HTTP Status Code가 500인 경우 에러를 Stack Trace와 함께 로깅하고 500 오류를 반환한다.

커스텀 에러 핸들러를 echo에 등록한다.  

```go
func main() {
  e := echo.New()
  e.HTTPErrorHandler = customHTTPErrorHandler // 커스텀 에러 핸들러 등록
  // ...
}
```

마지막으로 어떻게 에러를 반환해야 하는지 알아보자.  

```go
e.POST("/users", func(ctx echo.Context) error {  
  user := new(User)
  if err := ctx.Bind(user); err != nil {
    return ctx.JSON(http.StatusBadRequest, err.Error())
  }
  if err := ctx.Validate(user); err != nil {
    return ctx.JSON(http.StatusBadRequest, err.Error())
  }
  if err := UserService{}.CreateUser(ctx.Request().Context(), user); err != nil {
    return err // 500 오류
  }
  return ctx.JSON(http.StatusCreated, nil)
})
```

500 오류를 반환하고 싶은 경우 에러를 바로 반환한다. error를 반환하면 echo는 `customHTTPErrorHandler`에 에러 처리를 위임한다.

여기서 기억해야 할 것은 Stack Trace가 출력하기 위해서는 UserService에서 반환하는 error가 `github.com/pkg/errors `패키지여야 한다는 것이다.