Go 에러 핸들링 패턴 정리
배경
회사에서 트래픽이 많은 API 서버를 Node.js에서 Go로 마이그레이션하는 프로젝트를 시작했다. 재택근무로 전환되면서 코드 리뷰가 더욱 중요해진 시점이었는데, Go의 에러 핸들링 패턴에 대한 이해 부족으로 리뷰가 지연되는 경우가 많았다.
기본 패턴
Go는 예외가 아닌 반환값으로 에러를 처리한다.
func getUserByID(id string) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, err
}
return user, nil
}
에러 래핑
Go 1.13부터 fmt.Errorf의 %w 동사로 에러를 래핑할 수 있다.
if err != nil {
return fmt.Errorf("failed to get user %s: %w", id, err)
}
이렇게 하면 errors.Is, errors.As로 원본 에러를 확인할 수 있다.
커스텀 에러 타입
HTTP 상태 코드를 포함하는 커스텀 에러를 만들어 사용했다.
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
실전 적용
HTTP 핸들러에서는 이렇게 사용했다.
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(id)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
http.Error(w, appErr.Message, appErr.Code)
return
}
http.Error(w, "Internal Server Error", 500)
return
}
json.NewEncoder(w).Encode(user)
}
결론
명시적인 에러 처리는 처음엔 번거로웠지만, 에러 흐름을 추적하기 쉽고 예상치 못한 패닉이 줄어드는 장점이 있었다. 특히 프로덕션에서 에러 로깅과 모니터링이 훨씬 명확해졌다.