[Go] Error 처리는 어떻게 해야할까?

15270 단어 errorgolangerror

Don’t just check errors, handle them gracefully
Errors, 체크만 하지말고 우아하게 처리하세요.

너무 좋은 포스트 원문과 번역을 읽게되었다. 내가 이해한 내용을 바탕으로 요약해보고자 한다.

모든 설명과 소스코드는 원문과 번역을 참고하였다.

Error를 처리하는 나쁜 방식 2가지와 좋은 방식 1가지

나쁜 방식 1: sentinel error(파수꾼 에러)

if err == 특정_문자열_혹은_{
}

알고리즘 속에서 종료 조건을 나타내거나, 더 이상 처리가 불가능한 상황을 나타내는 값을 sentinel value라고 한다. 예를 들면 다음 코드의 -1은 sentinel value이다.

int find(int arr[], size_t len, int val)
{
    for (int i = 0; i < len; i++)
        if (arr[i] == val)
            return i;
    return -1; // not found
}

원문의 필자는 이 sentinel(파수꾼)이라는 단어를 error 와 조합하여, sentinel error라는 표현을 사용하였다. io.EOF, syscall 패키지에서 사용하는 syscall.ENOENT 같은 low level 의 error 들이 sentinel error에 속한다고 한다.

sentinel errors로 error를 처리할 경우, 함수를 호출한 쪽에서 if error == 특정값 연산을 통해서 에러를 처리한다.

이 "특정값"이 다양한 문제를 일으킨다. 필연적으로 호출자는 error를 검사하기위해 "특정값"을 import해야한다. 즉, package간에 의존성이 생긴다.

또는 이 sentinel error를 처리하는 로직 속에 error를 반환하는 interface를 정의했다면, 그 interface의 구현자들은 그 error만 을 반환하는 제약이 생긴다.

이런 다양한 문제들로 인해서 sentinel error는 사용은 최대한 피야하 한다.

나쁜 방식 2: error types

error를 특정 구조체로 만들어, error가 추가적인 context 정보를 포함하게 하는 방식이다.

type MyError struct {
        Msg string
        File string
        Line int
}

sentinel error와 다르게 일반적인 error에 다른 context정보를 wrapping하여 error를 처리할 수 있다.

하지만 error type 방식도 sentinel error가 가졌던 문제점을 그대로 가지고 있다.

error를 처리하는 패키지는 정의된 MyError 패키지에 의존성을 갖게 된다. 그리고 이런 error type를 활용하면 할 수록 의존관계는 더욱 강화되어, 결국 다시 악취나는 api가 만들어진다.

error types도 피해야하는 error 처리 방식이다.

좋은 방식: opaque error(불투명 에러)

if err != nil {
	return err
}

필자는 error처리의 해답으로 opaque error를 제안했다.

opaque error는 error가 발생하면 그대로 흘려보낸다. 다만 여기에 몇가지 장치를 추가하여 error를 처리한다.

우선, 행위 기반 error assertion을 추가했다.

error를 보고 작업 호출자가 작업을 재수행해야 하는지 판단하는 로직이 있다고 가정해보자.

일반적인 opaque error에서는 error를 보고 그 함수를 다시 호출할 수 없다. 그런데 error에 행위 메서드가 구현되어 있다면, 이런 로직 수행이 가능해진다.

type temporary interface {
        Temporary() bool
}
 
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
        te, ok := err.(temporary)
        return ok && te.Temporary()
}

IsTemporary()함수 내부에서 error는 temporary메서드가 구현되어 있다면 이를 호출하고, 그렇지 않으면 넘어간다. 그러면서 동시에 어떤 error든 IsTemporary()함수에 전달할 수 있기 때문에, 의존성 문제가 해결된다.

다음 장치는 Context의 추가이다.

go는 github.com/pkg/errors 패키지의 Wrap()과 Cause()함수로 Error에 Context를 추가하거나, 복원할 수 있다.

다음 코드는 context가 추가된 opaque error의 강력함을 보여준다.

package main
import (
    "github.com/pkg/errors"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
)
func ReadFile(path string) ([]byte, error) {
        f, err := os.Open(path)
        if err != nil {
								// os.Open에서 발생한 err에 "open failed"라는 context가 추가되었다.
                return nil, errors.Wrap(err, "open failed")
        }
        defer f.Close()
        buf, err := ioutil.ReadAll(f)
        if err != nil {
								// os.Open에서 발생한 err에 "read failed"라는 context가 추가되었다.
                return nil, errors.Wrap(err, "read failed")
        }
        return buf, nil
}
func ReadConfig() ([]byte, error) {
    home := os.Getenv("HOME")
    config, err := ReadFile(filepath.Join(home, ".settings.xml"))
// ReadFile에서 발생한 err에 "could not read config"라는 context가 추가되었다.
    return config, errors.Wrap(err, "could not read config")
}
func main() {
    _, err := ReadConfig()
    if err != nil {
            fmt.Println(err)
            os.Exit(1)
    }
}

만약 위의 ReadAll()에서 문제가 발생한다면, main()함수는 다음과 같은 결과를 출력한다.

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory

해당 메시지를 통해서 어떤 위치에서 어떤 에러가 발생했는지 한 눈에 확인할 수 있다.

주의할 점

에러는 한번만 처리하자

Write()함수가 log.Println()를 호출해서 err를 기록하고, err를 반환한다.

이럴 경우, Write함수를 호출한 함수는 다시 err를 받고 처리한다. 이 과정을 main()함수까지 거슬러 올라갈 경우, error로그는 매우 지저분해지고, 불필요한 동작이 반복된다.

func Write(w io.Writer, buf []byte) error {
        _, err := w.Write(buf)
        if err != nil {
                // annotated error goes to log file
                log.Println("unable to write:", err)
 
                // unannotated error returned to caller
                return err
        }
        return nil
}

결론

opaque error를 통해서 error를 처리하자.

만약 중간에 error를 검사해야한다면, error의 값이 아닌, 행위 구현자로 error를 검사하자.

errors.Wrap을 통해 error에 context 정보를 추가하자.(중간에 error를 점검할 필요가 있다면, errors.Cause로 복원한다.)

error 또한 public API의 일부이다. 이를 명심하여 최대한 의존성을 줄이는 방향으로 코드를 작성하자.

Reference

좋은 웹페이지 즐겨찾기