본문 바로가기

Go

고에서 에러 제어

프로그래밍에서 에러란 운영하고 있는 서비스에서 의도되지 않은 상태가 발생하는 걸 말한다.

에러가 발생하면 우리는 문제를 해결하기 위한 디버깅이라는 개 ㅈ같은 작업을 하게된다.

 

고에서는 기본적으로 아래와 같이 error 인터페이스가 Error() 메서드를 담고있는 형태로 내장이 되어있다.

type error interface {
   Error() string
}


error 는 보통 함수나 메서드에서 두번째 인자값에서 표현한다.

str := "kkk"
num , err := strconv.Atoi(str)
if err != nil {
   panic(err)
}

위에서는 kkk 를 int 타입으로 변경하는 strconv.Atoi() 를 사용하는데 kkk 는 숫자가 아니므로 에러를 뱉는다.
대부분의 패키지에서 저런형식으로 두번째 인자에 err 를 넣어서 에러를 캐치하게 된다.

 

독자적인 서비스를 운영하고 팀내에서 소통하기 위해 우리는 커스텀에러를 만들어 사용할 수 있다.


스트링기반 커스텀 에러

myFirstErr := errors.New("첫번째 에러")
panic(myFirstErr)

errors.New("") 를 사용하면 위와같이 에러메세지를 지정해 놓고 필요할때마다 가져다 쓸수 있다.

 

urlPath := "/api/auth/user/login"
mySecondErr := fmt.Errorf("this error from %s", urlPath)
panic(mySecondErr)

fmt.Errorf() 는 두개의 인자를 가지고 원하는 포맷을 만들어서 위와 같이 해당 API 에서 어떤 에러가 일어나는지 명시해 줄수도 있다.

 

데이터기반 커스텀 에러

고는 덕타이핑을 지원하기에 우리는 쉽게 커스텀에러를 구조체로 작성할수있다.

type PathError struct {
   Path string
   Version string
}

func(e *PathError) Error() string {
   return fmt.Sprintf("[version %v] error in path: %v",e.Version, e.Path)
}

func throwPathError(pe PathError) error {
   return &pe
}

func main() {
   newLoginPathError := PathError{Version :"2.2" ,Path: "/login"}
   panic(throwPathError(newLoginPathError))
}

위와 같이 PathError 구조체를 정의하고 Error() 메서드를 구현해주면
구조체안에 있는 데이터 값에 따라 커스텀에러를 쉽게 생성할 수 있다.

 

다양한 에러를 반환하기 위해서 하나하나 다 구현 할수 있지만 하드코딩은 지양해야 한다.

아래와 같은 두개의 PathError 와 HttpError 가 있고 메서드가 다 구현되어 있다고 가정할때
우리는 err.(type) 으로 에러들을 케이스별로 관리할수 있다.

type PathError struct {
   Path string
   Version string
}

type HttpError struct {
   StatusCode int
   Message string
}

func(e *HttpError) Error() string {
   return fmt.Sprintf("[%d][%v]",e.StatusCode,e.Message)
}

func(e *PathError) Error() string {
   return fmt.Sprintf("[version %v] error in path: %v",e.Version, e.Path)
}

func throwPathError(pe PathError) error {
   return &pe
}

func httpError(httpError HttpError) error {
   return &httpError
}

func main() {
   newLoginPathError := PathError{Version :"2.2" ,Path: "/login"}
   newHttpError := HttpError{StatusCode: 404, Message: "record not found"}
   err := httpError(newHttpError)
   err = throwPathError(newLoginPathError)
   if err != nil {
      switch err.(type) {
      case *PathError :
         fmt.Println(err)
      case *HttpError :
         fmt.Println(err)
      default:
         fmt.Println(err)
      }
   }
}

switch case 문으로 각각의 타입별 에러를 구현해 놓으면 에러로그를 쉽게 관리할 수 있다.

웹서버를 만든다고 가정할때 우리는 API 와 서버 사이에 위와 같은 에러제어를 할 수 있는 미들웨어를 하나두고 
센트리같은 에러 모니터링 툴을 적용해 디버깅 하는 시간을 줄일 수 있다. 

 

고언어는 다른 프로그래밍언어들처럼 예외처리가 없다.
하지만 비슷한 기능을 쉽게 제어할수 있는 defer , panic 그리고 recover 등이 있고 잘 알고 있어야 한다.

 

Defer

함수를 실행하면서 defer 키워드가 있으면 이 키워드 아래에 있는 모든걸 스택에 쌓고
함수가 끝날시 ( 에러가 나거나 return 으로 종료되거나 정상적으로 끝이날때) 해당 함수를 지연시키고
defer 스택에 있는걸 역순으로 실행시킨다.

예를들어 http 통신을 하고 난후 response 를 닫아줘야 하는데 defer 키워드가 없을시
중복된 코드를 짜야한다. ( 아무도 이렇게 하는 사람은 없음 )

 

아래 코드는 resp.Body.Close() 를 에러코드에 한번 함수가 끝나기전에 한번 호출하지만

resp, err := http.Get(url)

if err != nil {
   return err
}

ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
   resp.Body.Close()
   return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}

doc, err := html.Parse(resp.Body)
resp.Body.Close()

defer 는 함수가 끝나고 작동을 하니 저런식으로 한번만 작성해주면 된다.

resp, err := http.Get(url)

if err != nil {
   return err
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
   
   return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}

doc, err := html.Parse(resp.Body)

 

Panic

패닉은 현재 문제를 해결할 수 없을때 코드의 정상적인 실행 흐름을 중지하고
스택 추적을 포함하는 로그 메세지와 함께 프로그램이 종료된다.

panic("please stop program")

 

Recover

panic 은 프로그램의 실행을 중지하는거지만 개발자의 의도대로 중지하지 않고 panic이 일어났을때 복구가 되는 코드를 사용하는데
이때 필요한 기능이 recover다. REST API 서버에서는 굳이 사용하지 않지만 계속 연결되어 있어야 하는 소켓을 구현한 프로그램일 경우 에러를 발생시켜도 연결을 계속 유지해야 하거나 정상적으로 종료가 되게 짜기 위함이다.

recover 은 defer 와 같이 사용한다.

fmt.Println("시작")

defer func() {
   if r := recover() ; r != nil {
      fmt.Println("recover로 에러가 발생한 경우 로그")
   }
}()

panic("에러가 발생")

맨 아래에 패닉이 있지만 defer 로 쌓은 스택과 recover() 함수로 panic이 들어오면 프로그램이 멈추지 않고 recover() 안으로 들어가 나머지 코드를 실행한후 종료된다.

 

에러 감싸기 ( Error Wrapping) 

err := errors.New("원래 에러")
err = fmt.Errorf("더 거대한 에러는 %w 로부터 왔다.",err)
fmt.Println(err)

Errorf 의 %w 플래그로 err 를 기존 에러로부터 감싸서 새로운 에러를 반환할수 있다.

그리고 고언어는 이제 에러캐스팅을 겸비한