문제 상황

Golang으로 백엔드 개발한지 거의 1년이 되어가고 있는데, 처음 접했을 때부터 지금까지 항상 애매했던게 error 처리 방식이었다.

Golang에서는 기본적으로 프로그램이 죽는 exception(a.k.a Fatal Error)가 아닌 이상, 절대 panic(타 언어에서는 exception의 개념정도)을 던지지 말고, return value로 error를 리턴하도록 가이드를 하고 있다.

그래서 대부분 아래와 같이 if 분기문이 많다.

num1, err := strconv.Atoi("100") // 문자열 "100"을 숫자 100으로 변환
if err != nil {
	fmt.Printf("error occured, message: %s", err.Error())
} else {
	fmt.Println(num1, err)          // 100 <nil>
}

이런 형식의 코드로 인해 코드량이 증가하고, 함수가 깊어질 수록 에러를 계속 위로 올려보내야하는 번거로움도 많았다. 이러한 에러들을 로깅을 해야하는데, 언제 어디서 로깅을 해야하는지도 애매했다.

자바 스프링같이 에러를 찍을 때 자동으로 함수명이나 코드 위치가 기록되지 않아, 현업에서 예전 코드들은 에러가 발생한 모든 곳에서 로그를 찍고 있었다.

하지만 문제는 해당 에러가 발생한 곳에서는 해당 에러의 로그 레벨을 확실히 정할 수 없다는 것이었다. 유저를 찾지 못했을 경우, 어떤 상황에서는 해당 상황이 정상적인 로직으로 info 혹은 스킵, 또 다른 상황에서는 비정상적인 상황이라 error로 찍어야하는데, 이를 그 에러가 발생한 함수에서는 알지 못하는 경우가 그 예시이다.

만약 에러가 확실히 error 레벨일 경우에는 error로 찍고, 나머지 하위에서는 warn으로 찍는다고 해도, error로 찍은 함수 위에서도 해당 에러를 확인하여 분기해서 error, warn을 나눠찍어야하며, 중복적인 로그는 남아있다.(똑같거나 비슷한 에러를 올리기때문에)

func a() error {
	return errors.New("error")
}

func b() error {
	err := a()
	if err != nil {
		log.warn("error in b, message: %s", err.Error())
		return err
	}
	// ...
	return nil
}

func c() error {
	err := b()
	if err != nil {
		log.error("error in c, message: %s", err.Error())
		return err
	}
	// ...
	return nil
}

func main() {
	err := c()
	if error.Is(err, ...) {
		log.error("error in main, message: %s", err.Error())
	} else if error.Is(err, ...) {
		log.warn("error in main, message: %s", err.Error())
	}
	//...
}

그렇다면 에러를 어떻게 처리해야 코드도 깔끔해지고 중복적인 로그를 찍지 않을 수 있을까?

자주 쓰이는 Error는 type을 지정하기

우선 에러를 구분할 수 있도록 자주 쓰이는 error는 custom type으로 만드는 것이다. 어떤 에러가 내가 예상한 에러이고, 로직상 정상인지 비정상인지 알아낼 수 있어야한다.

아래는 mongo db에서 not found err를 구분하기 위해 만든 예시 타입이다.

type notFoundError struct {
	document string
	filter   interface{}
}

func NotFoundError(document string, filter map[string]string) *notFoundError {
	return &notFoundError{document: document, filter: filter}
}

func (e *notFoundError) Error() string {
	return fmt.Sprintf("%s not found, filter: %+v", e.document, e.filter)
}

// repository.go
func Find(username string) (*User, error) {
	return nil, errorType.NotFoundError("User", map[string]string{"id": username})
}

// User not found, filter: map[id:[email protected]]