Go/Go Basic

Ultimate-Go-06 [에러처리]

guiwoo 2023. 9. 19. 01:24

대부분의 고 패키지 또는 라이브러리를 보게 되면 결괏값과 error 인터페이스를 주로 반환되는 것을 상당 부분 많이 확인할 수 있다.

실제 이러한 에러처리가 미숙해 한번 서버를 패닉으로 내려버린 적도 있다.

 

이에 대해 고의 에러처리 철학과 그 방법에 대해 알아보자.


기본에러 값

type error interface {
	Error() string
}

고에서 빌트인으로 제공하는 에러 인터페이스이다. 

해당 error 패키지 에서 New를 이용해 error 포인터 구조체를 반환한다.

(error는 unexport 타입인데 어떻게 패키지에서 import 가 가능할까?- 빌트인 함수이기에 가능)

반환된 구조체는 errorString error 인터페이스를 만족하는 구현체가 있고 Error는 단순하게 메시지를 반환한다.

이렇게 에러메세지는 디커플링 되어있다. 왜? 클라이언트 즉 error를 구현하고 받는 모든 의사소통은

인터페이스로 이루어주기 때문이다.

 

가장 기본적인 에러 핸들링방식

func TestError01(t *testing.T) {
	webCall := func() error {
		n := time.Now().Unix()
		if n%2 == 0 {
			return nil
		}
		return errors.New("지금은 홀수 입니다.")
	}

	if err := webCall(); err != nil {
		fmt.Println(err)
		panic(err)
	}
	fmt.Println("Life is Good")
}

함수를 호출하고 반환되는 함수를 처리한다.


에러변수

위처럼 인터페이스를 만족만 시킨다면? 정말 손쉽게 커스텀 에러작성이 가능해진다. 

gorm에 정의되어 있는 커스텀 에러를 확인해 보면 

var (
	// ErrInvalidTransaction invalid transaction when you are trying to `Commit` or `Rollback`
	ErrInvalidTransaction = errors.New("invalid transaction")
    --- 생략 ---
)

이런 식으로 errors 패키지에서 제공해 주는 생성자함수를 이용하면 손쉽게 에러생성이 가능하다.

 

func TestError02(t *testing.T){
    --- 생략 webCall() ---
    if err := webCall(); err != nil {
        switch err {
        case ErrBadRequest:
            fmt.Println("Bad Request")
            return
        case ErrBadPageMoved:
            fmt.Println("The Page Moved")
            return
        }
    }
}

 

타입 컨택스트

빌트인에서 제공되는 에러 인터페이스보다 더많은 컨택스트가 필요한 경우도 존재한다. 

예를 들어 네트워킹 문제는 복잡할 수 있어, 에러 변수 만으로는 부족하다. 이럴 때 사용자 정의 타입을 통해 문제를 해결할 수 있다.

 

json 패키지중 unmarshal error 가 그중 하나이다.

type InvalidUnmarshalError struct {
	Type reflect.Type
}

func (e *InvalidUnmarshalError) Error() string {
	if e.Type == nil {
		return "json: Unmarshal(nil)"
	}

	if e.Type.Kind() != reflect.Pointer {
		return "json: Unmarshal(non-pointer " + e.Type.String() + ")"
	}
	return "json: Unmarshal(nil " + e.Type.String() + ")"
}

json 값을 언마샬링 할때 포인터의 값이 들어오지 않을 경우 반환값으로 해당 구조체가 사용된다.

 

func TestError03(t *testing.T) {
	type user struct {
		Name string
	}

	var u user
	err := json.Unmarshal([]byte(`{"name":"bill"}`), u)
	switch e := err.(type) {
	case *json.InvalidUnmarshalError:
		fmt.Printf("Invalid input type %v\n", e.Type)
	default:
		fmt.Println(err)
	}

	fmt.Println("Name", u.Name)
}

 


기능을 통한  컨택스트 처리

위에서 제공된 방법은 타입을 꺼내와서 사용하기에 디커플링으로부터 멀어진다. 디커플리를 유지하면서 타입단언을 막는 방법에 대해 알아보자.

type CustomOneError struct {
	message string
}

func (c *CustomOneError) Error() string {
	return "one error" + c.message
}

type CustomTwoError struct {
	message string
}

func (c *CustomTwoError) Error() string {
	return "two error" + c.message
}

func TestError04(t *testing.T) {
	errChecker := func() error {
		n := time.Now().Unix()
		if n%2 == 0 {
			return &CustomOneError{"wrong"}
		} else {
			return &CustomTwoError{"something"}
		}
	}
	err := errChecker()
	switch e := err.(type) {
	case *CustomOneError:
		if e.IsNotInteger(){
			fmt.Println("Not Integer",e.message)
		}
	case *CustomTwoError:
		if e.IsNotInteger(){
			fmt.Println("Not Integer",e.message)
		}
	}
}

이렇게 숫자가 아닌경우의 에러를 핸들링하기 위해서  swtich-case로 위에서 제공되는 방법으로 사용하는 경우이다.

숫자가 들어오는 경우에 대해서만 신경을 쓴다고 가정한다면 해당 switch는 인터페이스로 포장할 수 있다.

func TestError04(t *testing.T) {
	--- 생략 ---
	switch e := err.(type) {
	case ErrNotInteger:
		fmt.Println("Not Integer Error ", e.IsNotInteger())
	default:
		fmt.Println("default ", err)
	}
}

에러포장

github.com/pkg/errors.Wrap()  해당 패키지를 이용하면 고에서 제공하는 일반 렙핑이 아닌 스택트레이스가 제공되는 에러를 반환할 수 있다.

func TestError05(t *testing.T) {
	thirdCall := func(i int) error {
		return errors.New("99")
	}
	secondCall := func(i int) error {
		if err := thirdCall(i); err != nil {
			return fmt.Errorf()
		}
		return nil
	}
	firstCall := func(i int) error {
		if err := secondCall(i); err != nil {
			return pkgErr.Wrap(err, "Second Call Error")
		}
		return nil
	}
	for i := 0; i < 5; i++ {
		if err := firstCall(i); err != nil {
			fmt.Println("stack trace")
			fmt.Printf("%+v\n", err)
		}
	}
}

//고 기본 패지키이용 방법
return fmt.Errorf("third call error %w", err)

기본 패키지를 이용해서 랩핑이 가능하지만, 스택트레이스 없이 에러로그를 확인하는것은 상단 한 피로감을 유발할 것으로 보인다.

이렇게 처리된 에러는 에러의 근원지를 확인할수 있는. Cause method를 제공한다.

func TestError05(t *testing.T) {
     --- 생략 --- 
	for i := 0; i < 1; i++ {
		if err := firstCall(i); err != nil {
			switch v:= pkgErr.Cause(err).(type){
			case *ErrCustomOne:
				fmt.Println("custom One error")
			}
			--- 생략 --- 
		}
	}
}

이 방법외에도 errors 기본패키지에서 as 메서드를 제공한다.

func TestError05(t *testing.T) {

	thirdCall := func(i int) error {
		return &CustomOneError{"hoit"}
	}
	secondCall := func(i int) error {
		if err := thirdCall(i); err != nil {
			return fmt.Errorf("third call error %w", err)
		}
		return nil
	}
	firstCall := func(i int) error {
		if err := secondCall(i); err != nil {
			return fmt.Errorf("second call error %w", err)
		}
		return nil
	}
	for i := 0; i < 1; i++ {
		if err := firstCall(i); err != nil {
			var v *CustomOneError
			switch {
			case errors.As(err, &v):
				fmt.Println("custom One error found")
			}

			fmt.Println("Stack Trace")
			fmt.Printf("%+v\n", err)
			fmt.Printf("Stack Done")
		}
	}
}

as를 사용하면 기본 빌트인 타입으로도 충분히 에러 발생 근원지에 대해 트랙킹 할 수 있다.

 

이렇게 다양한 error를 go 에서는 지원해주고 있다. 

왜? 명시적으로 err를 처리하기를 바라기 때문이다. 다시 말해 err 처리를 회피하지 않고 제대로 처리하길 바라는 마음에서 위와 같은 다양한 빌트인 함수를 제공해 주고, 또 계속 패키지를 발전시켜 나가는 것이 아닌가 싶다.


출처: https://github.com/hoanhan101/ultimate-go/tree/master/go/design

출처: https://ultimate-go-korean.github.io/translation/#%EC%97%90%EB%9F%AC-%ED%8F%AC%EC%9E%A5wrapping-errors