Backend
Go 에러 핸들링 전략 실무 가이드
Go에서 errors.Is/As, 래핑, 도메인 에러를 실무적으로 적용하는 기준을 정리합니다.
Go 에러 핸들링 전략 실무 가이드
핵심 원칙
- 에러는 문자열이 아니라 의미 있는 값으로 다룹니다.
- 상위 계층으로 올릴 때는
%w로 원인 체인을 유지합니다. - 분기 판단은 문자열 비교가 아니라
errors.Is,errors.As를 사용합니다.
1) 래핑: %w를 기본으로
func readConfig(path string) error {
if _, err := os.ReadFile(path); err != nil {
return fmt.Errorf("config read failed: %w", err)
}
return nil
}
%v를 쓰면 원인 에러를 잃어버립니다. 에러 전달 경로에서는 %w를 기본으로 두세요.
2) errors.Is로 분기
var ErrNotFound = errors.New("not found")
func findUser(id string) (*User, error) {
u, err := repo.Find(id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("find user: %w", err)
}
return u, nil
}
호출부는 체인 전체에서 의미를 찾습니다.
if errors.Is(err, ErrNotFound) {
// 404
}
3) errors.As로 타입 추출
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %s", e.Field, e.Msg)
}
var vErr *ValidationError
if errors.As(err, &vErr) {
log.Printf("field=%s msg=%s", vErr.Field, vErr.Msg)
}
4) 도메인 에러로 HTTP 매핑
type Code string
const (
CodeNotFound Code = "NOT_FOUND"
CodeConflict Code = "CONFLICT"
)
type DomainError struct {
Code Code
Msg string
Cause error
}
func (e *DomainError) Error() string { return e.Msg }
func (e *DomainError) Unwrap() error { return e.Cause }
핸들러에서는 도메인 코드만 보고 응답을 결정합니다.
func toStatus(code Code) int {
switch code {
case CodeNotFound:
return http.StatusNotFound
case CodeConflict:
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}
운영에서 중요한 규칙
- 에러 메시지는 사용자 메시지와 내부 로그 메시지를 분리합니다.
- 재시도 여부(temporary/permanent)를 코드 또는 타입으로 표현합니다.
- 로그는 "한 번만" 책임 계층에서 남깁니다. (중복 로깅 금지)
피해야 할 패턴
if err != nil { return errors.New("...") }로 원인 손실err.Error()문자열 포함 여부로 분기- 패키지 전역 센티넬 에러를 과도하게 남발
요약
Go 에러 전략의 핵심은 단순합니다.
- 래핑은
%w - 분기는
Is/As - 외부 계약은 도메인 에러 코드
이 세 가지를 팀 규칙으로 고정하면 장애 대응 속도와 코드 일관성이 크게 좋아집니다.