커스텀 에러 타입
단순 문자열 에러로는 부족할 때, 구조체 기반의 커스텀 에러 타입을 만들면 에러에 풍부한 정보를 담을 수 있습니다. 센티넬 에러, 에러 코드 패턴과 함께 실무에서 자주 사용되는 패턴들을 살펴봅니다.
구조체 기반 커스텀 에러
Error() string 메서드를 구현하면 error 인터페이스를 만족합니다.
package main
import (
"errors"
"fmt"
)
// 기본 커스텀 에러
type AppError struct {
Code int
Message string
Err error // 원인 에러 (선택적)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Unwrap 구현 — errors.Is/As가 체인을 탐색할 수 있게 함
func (e *AppError) Unwrap() error {
return e.Err
}
// 에러 생성 헬퍼 함수
func NewAppError(code int, msg string, cause error) *AppError {
return &AppError{Code: code, Message: msg, Err: cause}
}
var ErrDBFailed = errors.New("database operation failed")
func fetchData(id int) (string, error) {
if id <= 0 {
return "", &AppError{Code: 400, Message: "invalid id"}
}
if id > 1000 {
return "", &AppError{
Code: 500,
Message: "fetch failed",
Err: ErrDBFailed,
}
}
return fmt.Sprintf("data-%d", id), nil
}
func main() {
// 성공
data, err := fetchData(42)
if err == nil {
fmt.Println("데이터:", data)
}
// 잘못된 입력
_, err = fetchData(-1)
if err != nil {
fmt.Println("에러:", err) // [400] invalid id
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("코드: %d, 메시지: %s\n", appErr.Code, appErr.Message)
}
}
// DB 실패 (래핑 포함)
_, err = fetchData(9999)
if err != nil {
fmt.Println("에러:", err) // [500] fetch failed: database operation failed
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("코드: %d\n", appErr.Code)
}
// Unwrap으로 원인 에러 탐색 가능
fmt.Println("DB 실패?", errors.Is(err, ErrDBFailed)) // true
}
}
센티넬 에러 패턴
패키지 수준에서 미리 정의된 에러 값으로, errors.Is와 함께 사용합니다.
package main
import (
"errors"
"fmt"
)
// 패키지 수준 센티넬 에러 — Err 접두사 관례
var (
// 일반적인 센티넬 에러
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrBadRequest = errors.New("bad request")
ErrTimeout = errors.New("timeout")
ErrCanceled = errors.New("canceled")
)
// 도메인별 센티넬 에러
var (
ErrUserNotFound = errors.New("user not found")
ErrUserSuspended = errors.New("user suspended")
ErrEmailDuplicate = errors.New("email already registered")
ErrPasswordInvalid = errors.New("invalid password")
)
// 간단한 사용자 스토어
type UserStore struct {
users map[string]string // email → password
}
func NewUserStore() *UserStore {
return &UserStore{users: map[string]string{
"alice@example.com": "secret123",
}}
}
func (s *UserStore) Register(email, password string) error {
if email == "" || password == "" {
return fmt.Errorf("register: %w", ErrBadRequest)
}
if _, exists := s.users[email]; exists {
return fmt.Errorf("register %q: %w", email, ErrEmailDuplicate)
}
s.users[email] = password
return nil
}
func (s *UserStore) Login(email, password string) error {
pwd, exists := s.users[email]
if !exists {
return fmt.Errorf("login %q: %w", email, ErrUserNotFound)
}
if pwd != password {
return fmt.Errorf("login %q: %w", email, ErrPasswordInvalid)
}
return nil
}
func main() {
store := NewUserStore()
// 정상 등록
if err := store.Register("bob@example.com", "pass456"); err != nil {
fmt.Println("등록 실패:", err)
} else {
fmt.Println("등록 성공: bob@example.com")
}
// 중복 이메일
if err := store.Register("alice@example.com", "newpass"); err != nil {
fmt.Println("등록 실패:", err)
if errors.Is(err, ErrEmailDuplicate) {
fmt.Println("→ 이미 사용 중인 이메일입니다")
}
}
// 로그인 시도
attempts := []struct{ email, pass string }{
{"alice@example.com", "secret123"},
{"alice@example.com", "wrongpass"},
{"nobody@example.com", "pass"},
}
for _, a := range attempts {
err := store.Login(a.email, a.pass)
if err == nil {
fmt.Printf("로그인 성공: %s\n", a.email)
continue
}
switch {
case errors.Is(err, ErrUserNotFound):
fmt.Printf("로그인 실패 (%s): 사용자 없음\n", a.email)
case errors.Is(err, ErrPasswordInvalid):
fmt.Printf("로그인 실패 (%s): 잘못된 비밀번호\n", a.email)
default:
fmt.Printf("로그인 실패 (%s): %v\n", a.email, err)
}
}
}
에러 코드 패턴
HTTP API나 gRPC 서비스에서 자주 사용하는 에러 코드 기반 패턴입니다.
package main
import (
"errors"
"fmt"
)
// 에러 코드 타입
type ErrorCode int
const (
CodeOK ErrorCode = 0
CodeBadRequest ErrorCode = 400
CodeUnauthorized ErrorCode = 401
CodeForbidden ErrorCode = 403
CodeNotFound ErrorCode = 404
CodeConflict ErrorCode = 409
CodeInternal ErrorCode = 500
)
func (c ErrorCode) String() string {
switch c {
case CodeOK:
return "OK"
case CodeBadRequest:
return "BAD_REQUEST"
case CodeUnauthorized:
return "UNAUTHORIZED"
case CodeForbidden:
return "FORBIDDEN"
case CodeNotFound:
return "NOT_FOUND"
case CodeConflict:
return "CONFLICT"
case CodeInternal:
return "INTERNAL"
default:
return fmt.Sprintf("UNKNOWN(%d)", int(c))
}
}
// 코드 기반 에러 타입
type CodedError struct {
Code ErrorCode
Message string
Detail string
cause error
}
func (e *CodedError) Error() string {
if e.Detail != "" {
return fmt.Sprintf("%s: %s (%s)", e.Code, e.Message, e.Detail)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *CodedError) Unwrap() error { return e.cause }
// 편의 생성 함수들
func NotFound(resource, id string) *CodedError {
return &CodedError{
Code: CodeNotFound,
Message: fmt.Sprintf("%s not found", resource),
Detail: fmt.Sprintf("id=%s", id),
}
}
func Unauthorized(reason string) *CodedError {
return &CodedError{
Code: CodeUnauthorized,
Message: "authentication required",
Detail: reason,
}
}
func BadRequest(msg string) *CodedError {
return &CodedError{
Code: CodeBadRequest,
Message: "invalid request",
Detail: msg,
}
}
func Internal(cause error) *CodedError {
return &CodedError{
Code: CodeInternal,
Message: "internal server error",
cause: cause,
}
}
// 에러 코드 추출 헬퍼
func GetCode(err error) ErrorCode {
var coded *CodedError
if errors.As(err, &coded) {
return coded.Code
}
return CodeInternal
}
// 예시 서비스
func getProduct(id string) (string, error) {
if id == "" {
return "", fmt.Errorf("getProduct: %w", BadRequest("id is required"))
}
if id == "999" {
return "", fmt.Errorf("getProduct: %w", NotFound("product", id))
}
return fmt.Sprintf("Product-%s", id), nil
}
func main() {
testIDs := []string{"", "42", "999"}
for _, id := range testIDs {
product, err := getProduct(id)
if err != nil {
code := GetCode(err)
fmt.Printf("에러 [%d %s]: %v\n", int(code), code, err)
var coded *CodedError
if errors.As(err, &coded) {
fmt.Printf(" → 코드: %d, 메시지: %s, 상세: %s\n",
int(coded.Code), coded.Message, coded.Detail)
}
} else {
fmt.Printf("제품: %s\n", product)
}
}
}
타입 기반 에러 계층 구조
복잡한 애플리케이션에서 에러 계층을 구성하는 패턴입니다.
package main
import (
"errors"
"fmt"
)
// 최상위 에러 인터페이스 (선택적으로 확장)
type DomainError interface {
error
Domain() string // 에러가 발생한 도메인
Temporary() bool // 재시도 가능 여부
}
// 기본 도메인 에러 구현
type baseDomainError struct {
domain string
msg string
temp bool
cause error
}
func (e *baseDomainError) Error() string {
if e.cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.domain, e.msg, e.cause)
}
return fmt.Sprintf("[%s] %s", e.domain, e.msg)
}
func (e *baseDomainError) Domain() bool { return e.domain != "" }
func (e *baseDomainError) DomainName() string { return e.domain }
func (e *baseDomainError) Temporary() bool { return e.temp }
func (e *baseDomainError) Unwrap() error { return e.cause }
// 도메인별 구체 에러 타입
type AuthError struct {
baseDomainError
UserID string
}
func NewAuthError(msg, userID string, cause error) *AuthError {
return &AuthError{
baseDomainError: baseDomainError{
domain: "auth",
msg: msg,
temp: false,
cause: cause,
},
UserID: userID,
}
}
type NetworkError struct {
baseDomainError
URL string
}
func NewNetworkError(msg, url string, temporary bool, cause error) *NetworkError {
return &NetworkError{
baseDomainError: baseDomainError{
domain: "network",
msg: msg,
temp: temporary,
cause: cause,
},
URL: url,
}
}
// 처리 함수
func processRequest(userID, url string) error {
if userID == "" {
return NewAuthError("user not authenticated", userID, nil)
}
if url == "bad-host" {
return fmt.Errorf("processRequest: %w",
NewNetworkError("connection refused", url, true, nil))
}
return nil
}
func main() {
cases := []struct{ user, url string }{
{"alice", "api.example.com"},
{"", "api.example.com"},
{"bob", "bad-host"},
}
for _, c := range cases {
err := processRequest(c.user, c.url)
if err != nil {
fmt.Printf("에러: %v\n", err)
// 타입별 처리
var authErr *AuthError
var netErr *NetworkError
switch {
case errors.As(err, &authErr):
fmt.Printf(" → 인증 에러, 사용자: %q\n", authErr.UserID)
case errors.As(err, &netErr):
fmt.Printf(" → 네트워크 에러, URL: %q, 재시도 가능: %v\n",
netErr.URL, netErr.Temporary())
}
} else {
fmt.Printf("성공: user=%q url=%q\n", c.user, c.url)
}
}
}
핵심 정리
Error() string메서드를 구현하면error인터페이스 충족Unwrap() error구현으로errors.Is/As가 체인 탐색 가능- 센티넬 에러는
Err접두사, 패키지 수준에 선언- 에러 코드 패턴: HTTP API나 gRPC 응답 코드와 매핑
- 복잡한 도메인은 계층적 에러 타입으로 구조화
errors.As로 구체 타입을 추출해 상세 정보 활용