Golang Error Stack Trace와 로깅

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

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

Untitled

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

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

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

아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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("오류 발생!!")
}

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

1
오류 발생!!

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

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

github.com/pkg/errors

Stack Trace를 출력하는 방법은 Go가 기본적으로 제공하는 errors 패키지 대신 github.com/pkg/errors 를 사용하는 것이다.

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

1
go get github.com/pkg/errors

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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가 출력되는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
오류 발생!!
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 패키지로 Stack Trace를 출력할 수 있다. 하지만 내가 만들지 않고 사용만 하는 코드(예.Go SDK나 github에서 받은 모듈) 에서 반환받은 error는 어떻게 처리해야 할까?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 없이 오류 메시지만 출력하는 것을 확인할 수 있다.

1
invalid character ',' after object key

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

1
2
3
4
5
6
7
8
9
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와 오류 메시지가 함께 출력되는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
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 웹 프레임워크(이하 Echo)에서 Error가 발생하면 자동으로 HTTP Status Code를 500(Internal Server Error)을 반환하고 Stack Trace를 출력하게 만들어 보자.

먼저 Echo의 기본 에러 핸들러(Default HTTP Error Handler)를 대체하는 커스텀 에러 핸들러(Custom HTTP Error Handler)를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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에 등록한다.

1
2
3
4
5
func main() {
  e := echo.New()
  e.HTTPErrorHandler = customHTTPErrorHandler // 커스텀 에러 핸들러 등록
  // ...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 패키지여야 한다는 것이다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.