📖 이 글은 Saturday Night 스터디에서 Concurrency in Go를 주제로 발표하기 위해 만들어졌습니다.
에러
사용자가 요청한 작업을 프로그램이 수행할 수 없는 상태에 들어갔음을 나타냅니다.
개발자들이 프로그램을 돌아가도록 기능을 만드는 것에는 집중하지만 에러가 발생했을 때 이를 처리하는 것을 크게 신경쓰지 않는 경향이 있다고 저자는 설명합니다.
프로그램을 구성할 때 에러 또한 신경써서 처리하여 사용자 친화적인 프로그램을 만들어야하는 것이 개발자의 역할입니다.
에러는 다음과 같이 두 가지로 분류 합니다.
저자는 프로그램에서 처리되지 않은 에러들을 버그로 취급하고 이를 사용자에게 바로 전파되지 않도록 프로그램에서 처리해야 한다고 이야기합니다.
에러는 원활한 분석을 위해 아래와 같이 중요한 정보를 포함해야 합니다.
위와 같은 정보는 기본적으로 에러에 포함되지 않기 때문에 에러 처리 시스템을 직접 구축하거나 범용적인 프레임워크를 사용하는 것을 고민해야 합니다.
(여러 모듈로 구성된 시스템 구성도)
코어 모듈에서 발생한 에러는 코어 모듈의 컨텍스트 내에서는 올바른 형식으로 간주될 수 있지만 프로그램 전체의 컨텍스트 내에서는 알 수 없는 에러로 간주될 수 있습니다.
에러가 발생한 근본적인 원인의 세부 정보는 에러가 최초 인스턴스화 될 때 포함되지만, 프로그램 전반적으로 에러를 식별하기 위해서는 에러에 식별 정보를 포함하여 약속된 에러 형태를 반환해야 합니다.
예)
func PostReport(id string) error {
result, err := lowlevel.DoWork();
if err != nil {
if _, ok := err.(lowlevel.Error); ok { // 1
err = WrapErr(err, "cannot post report with id %q", id) // 2
}
return err
}
// do work
}
이를 통해 에러의 정확성을 높혀 프로그램이 에러에 대해 대응할 수 있게 되고, 식별할 수 없는 타입의 에러가 발생하는 경우 버그로 취급할 수 있게 됩니다.
위와 같은 형태로 프레임워크를 구축해보겠습니다.
// err_ext
type MyError struct {
Inner error
Message string
StackTrace string
Misc map[string]interface{}
}
func (err MyError) Error() string {
return err.Message
}
func wrapError(err error, msgF string, msgArgs ...interface{}) MyError {
return MyError {
Inner: err, // 1
Message: fmt.SprintF(msgF, msgArgs...),
StackTrace: string(debug.Stack()), // 2
Misc: make(map[string]interface{}), // 3
}
}
// lowlevel 모듈 (코어 구성 요소)
type LowLevelErr struct {
error
}
func isGloballyExec(path string) (bool, error) {
info, err := os.Stat(path)
if err != nil {
return false, LowLevelErr{(wrapError(err, err.Error()))} // 1
}
return info.Mode().Perm() & 0100 == 0100, nil
}
// intermediate 모듈 (중개자 구성 요소)
type IntermediateErr struct {
error
}
func runJob(id string) error {
const jobBinPath = "/bad/job/binary"
isExecutable, err := isGloballyExec(jobBinPath)
if err != nil {
return err // 1
} else if isExecutable == false {
return wrapError(nil, "job binary is not executable")
}
return exec.Command(jobBinPath, "--id=" + id).Run() // 1
}
// main (CLI 구성 요소)
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ltime | log.LUTC)
err := runJob("1")
if err != nil {
msg := "There was an unexpected issue; please report this as a bug."
if _, ok := err.(IntermediateErr); ok { // 1
msg = err.Error()
}
handleError(1, err, msg) // 2
}
}
func handleError(key int, err error, msg string) {
log.SetPrefix(fmt.Sprintf("[logID: %v]: ", key))
log.Printf("%#v", err) // 3
fmt.Printf("[errorID: %v] %v", key, msg)
}
// log
[logID: 1]: 03:16:32 lowlevel.LowLevelErr{error:err_ext.MyError{
Inner:(*fs.PathError)(0xc000098180),
Message:"stat /bad/job/binary: no such file or directory%!(EXTRA []interface {}=[])",
StackTrace:"goroutine 1 [running]:
runtime/debug.Stack(0xc0000b0030, 0x2f, 0xc000092d48)
/Users/johnny/.gvm/gos/go1.16/src/runtime/debug/stack.go:24 +0x9f
saturday-night/src/err_ext.WrapError(0x1101e68, 0xc000098180, 0xc0000b0030, 0x2f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
/Users/johnny/git_workspace/saturday-night/src/err_ext/my_error.go:23 +0xef
saturday-night/src/lowlevel.IsGloballyExec(0x10e0965, 0xf, 0x3, 0x1005ecb, 0xc000056058)
/Users/johnny/git_workspace/saturday-night/src/lowlevel/lowlevel.go:16 +0xc5
saturday-night/src/intermediate.RunJob(0x10df028, 0x1, 0x1018301, 0x0)
/Users/johnny/git_workspace/saturday-night/src/intermediate/intermediate.go:15 +0x48 // 에러 인스턴스 생성
main.main()
/Users/johnny/git_workspace/saturday-night/src/main.go:14 +0x70",
Misc:map[string]interface {}{}}
}
// message
[errorID: 1] There was an unexpected issue; please report this as a bug.
작성한 프로그램을 실행시켜보면 위와 같이 로그에는 에러의 원인을 분석할 수 있도록 스택 트레이스를 기록하며, 사용자에게는 사용자 친화적인 메시지를 전달합니다.
사용자는 프로그램을 이용하다가 에러가 발생하면 errorID를 첨부하여 원활하지 못했던 서비스 이용에 대한 원인 분석을 요구할 수 있고, 개발자는 해당 식별값을 통해 로그를 확인하여 분석 결과를 제공하고 프로그램을 개선할 수 있습니다.
개발자는 사용자와의 커뮤니케이션을 통해 버그를 찾아 퇴치하여 프로그램을 개선할 수 있습니다.
위의 로그를 분석하면 intermediate.go 15번 라인에서 에러가 발생했음을 알 수 있습니다.
// intermediate 모듈
func runJob(id string) error {
const jobBinPath = "/bad/job/binary"
isExecutable, err := isGloballyExec(jobBinPath)
if err != nil {
return IntermediateErr{
wrapError(err, "cannot run job %q: requisite binaries not available", id)
} // 1
} else if isExecutable == false {
return wrapError(nil, "cannot run job %q: requisite binaries are not executable", id)
}
return exec.Command(jobBinPath, "--id=" + id).Run() // 1
}
//log
[logID: 1]: 04:19:25 intermediate.IntermediateErr{error:err_ext.MyError{
Inner:lowlevel.LowLevelErr{
error:err_ext.MyError{
Inner:(*fs.PathError)(0xc000098180),
Message:"stat /bad/job/binary: no such file or directory%!(EXTRA []interface {}=[])",
StackTrace:"goroutine 1 [running]:
runtime/debug.Stack(0xc0000b0030, 0x2f, 0xc000092d18)
/Users/johnny/.gvm/gos/go1.16/src/runtime/debug/stack.go:24 +0x9f
saturday-night/src/err_ext.WrapError(0x1102148, 0xc000098180, 0xc0000b0030, 0x2f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
/Users/johnny/git_workspace/saturday-night/src/err_ext/my_error.go:23 +0xef
saturday-night/src/lowlevel.IsGloballyExec(0x10e0bbe, 0xf, 0x55, 0x13bec40, 0x1012c1b)
/Users/johnny/git_workspace/saturday-night/src/lowlevel/lowlevel.go:16 +0xc5
saturday-night/src/intermediate.RunJob(0x10df288, 0x1, 0x1018301, 0x0)
/Users/johnny/git_workspace/saturday-night/src/intermediate/intermediate.go:15 +0x48
main.main()
/Users/johnny/git_workspace/saturday-night/src/main.go:14 +0x70",
Misc:map[string]interface {}{}
}
},
Message:"cannot run job ["1"]: requisite binaries not available",
StackTrace:"goroutine 1 [running]:
runtime/debug.Stack(0x10e69b4, 0x33, 0xc000092df0)
/Users/johnny/.gvm/gos/go1.16/src/runtime/debug/stack.go:24 +0x9f
saturday-night/src/err_ext.WrapError(0x11022c8, 0xc000096280, 0x10e69b4, 0x33, 0xc000096290, 0x1, 0x1, 0x0, 0x0, 0x0, ...)
/Users/johnny/git_workspace/saturday-night/src/err_ext/my_error.go:23 +0xef
saturday-night/src/intermediate.RunJob(0x10df288, 0x1, 0x1018301, 0x0)
/Users/johnny/git_workspace/saturday-night/src/intermediate/intermediate.go:18 +0x319
main.main()
/Users/johnny/git_workspace/saturday-night/src/main.go:14 +0x70",
Misc:map[string]interface {}{}}
}
// message
[errorID: 1] cannot run job ["1"]: requisite binaries not available
// 1번 작업을 실행할 수 없음: 필요한 바이너리를 사용할 수 없습니다.
이전의 raw 에러를 Wrapping하여 추가적인 처리를 한 내역을 로그를 통해 알 수 있으며, 메세지 또한 사용자가 최소한의 원인은 알 수 있도록 변경되었습니다.
이러한 에러 처리 접근방식의 패키지들이 존재하지만github.com/pkg/errors, 위와 같이 유기적인 시스템을 직접 구축할 수도 있습니다.
중요한건 기능을 만드는 것과 같은 수준의 관심으로 에러를 다루어야하며 버그와 올바른 에러를 구분짓고 점진적으로 버그가 없는 시스템으로 개선해 나아가야 합니다.