Go API에서 사용자 정의 오류 및 HTTP 상태 코드 제작
Grace Collins
Solutions Engineer · Leapcell

소개
견고하고 유지보수 가능한 API를 구축하는 것은 현대 소프트웨어 개발의 초석입니다. 종종 간과되거나 최소한 충분한 주의를 받지 못하는 중요한 측면은 효과적인 오류 처리입니다. Go API에서 문제가 발생했을 때 단순히 일반적인 "500 내부 서버 오류"를 반환하는 것은 클라이언트가 추측하게 만들어 개발자 경험을 저하시키고 디버깅 주기를 어렵게 만들 수 있습니다. 대신, 특정하고 실행 가능한 오류 메시지와 정확한 HTTP 상태 코드를 제공하면 API의 사용성과 진단 기능을 크게 향상시킬 수 있습니다. 이 문서는 Go에서 사용자 정의 오류 유형을 만드는 방법과 더 중요하게는 이러한 내부 오류를 API 소비자에게 의미 있는 HTTP 상태 코드로 우아하게 변환하여 보다 투명하고 사용자 친화적인 상호 작용을 촉진하는 방법을 살펴봅니다.
핵심 개념 이해
구현에 대해 자세히 알아보기 전에 논의의 기초가 될 몇 가지 주요 용어를 간략하게 정의해 보겠습니다.
- 오류 인터페이스 (Go): Go에서 오류는 단일 메서드
Error() string을 가진 내장error인터페이스를 구현하는 값입니다. 이 단순성은 매우 강력하여 사용자 정의 오류 표현을 매우 유연하게 만들 수 있습니다. - 사용자 정의 오류 유형: 기본
error인터페이스 외에 사용자 정의 오류 유형은error인터페이스를 구현하는 사용자 정의 구조체입니다. 이를 통해 오류 자체에 오류 코드, 사용자 친화적인 메시지 또는 스택 추적과 같은 추가 컨텍스트를 직접 포함할 수 있습니다. - HTTP 상태 코드: HTTP 응답 헤더에 있는 세 자리 숫자로서 서버가 클라이언트의 요청을 충족하려는 시도의 상태를 나타냅니다. 이러한 코드는 범주화됩니다 (예: 성공에는 2xx, 클라이언트 오류에는 4xx, 서버 오류에는 5xx) 그리고 API 호출 결과를 전달하는 표준화된 방법을 제공합니다.
- 오류 매핑: 내부 애플리케이션 오류 (사용자 정의 Go 오류 유형일 수 있음)를 API 클라이언트를 위한 적절한 HTTP 상태 코드 및 해당 오류 메시지로 변환하는 프로세스.
우아한 오류 매핑의 원칙
목표는 클라이언트가 무엇이 잘못되었는지 이해할 수 있도록 충분한 정보를 제공하는 동시에 민감한 내부 세부 정보를 노출하지 않는 것입니다. 여기에는 다음이 포함됩니다.
- 구체성: 다양한 유형의 오류를 구분합니다 (예: 잘못된 입력, 무단 액세스, 리소스를 찾을 수 없음).
- 컨텍스트: 문제를 설명하는 명확하고 간결한 메시지를 제공합니다.
- 실행 가능성: 가능한 경우 클라이언트에게 문제를 해결하는 방법을 안내합니다.
- 표준화: 잘 알려진 HTTP 상태 코드를 사용하여 기존 클라이언트 측 오류 처리 패턴을 활용합니다.
사용자 정의 오류 및 매핑 구현
실제 예제를 통해 이러한 원칙을 설명해 보겠습니다. 사용자 계정을 관리하는 API를 상상해 보세요.
1. 사용자 정의 오류 유형 정의
먼저 특정 API 오류 조건을 나타내는 사용자 정의 오류 유형을 정의합니다.
package user import ( "fmt" "net/http" ) // UserError represents a custom error for user-related operations. type UserError struct { Code string // A unique application-specific error code Message string // A user-friendly message Status int // The corresponding HTTP status code Err error // The underlying error, if any } // Error implements the error interface for UserError. func (e *UserError) Error() string { if e.Err != nil { return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err) } return fmt.Sprintf("[%s] %s", e.Code, e.Message) } // Unwrap allows checking for the underlying error. func (e *UserError) Unwrap() error { return e.Err } // Standard custom error instances var ( ErrUserNotFound = &UserError{Code: "USER-001", Message: "User not found", Status: http.StatusNotFound} ErrInvalidCredentials = &UserError{Code: "AUTH-002", Message: "Invalid credentials provided", Status: http.StatusUnauthorized} ErrUserAlreadyExists = &UserError{Code: "USER-003", Message: "A user with the provided email already exists", Status: http.StatusConflict} ErrInvalidInput = &UserError{Code: "VALID-004", Message: "Invalid request payload or parameters", Status: http.StatusBadRequest} ErrInternal = &UserError{Code: "SERVER-005", Message: "An unexpected error occurred", Status: http.StatusInternalServerError} ) // NewUserErrorFromHttpStatus creates a general UserError from an HTTP status code and a message. func NewUserErrorFromHttpStatus(status int, message string) *UserError { code := fmt.Sprintf("HTTP-%d", status) // You might have a more sophisticated mapping for codes here return &UserError{Code: code, Message: message, Status: status} }
이 코드에서 UserError는 사용자 정의 오류 구조체입니다. 내부 식별을 위한 Code, API 클라이언트를 위한 Message, HTTP 응답을 위한 Status, 그리고 기본 Go 오류를 래핑하기 위한 선택적 Err를 포함합니다. 또한 일반적인 시나리오에 대한 몇 가지 사전 정의된 UserError 인스턴스를 정의합니다.
2. 비즈니스 로직에서 사용자 정의 오류 반환
이제 서비스 계층에서 이러한 사용자 정의 오류를 반환할 수 있습니다.
package service import ( "database/sql" "errors" "your_module/your_app/user" // Assuming your custom errors are here ) type UserService struct { // ... dependencies } func (s *UserService) GetUserByID(id string) (*user.User, error) { // Simulate data store interaction if id == "" { return nil, user.ErrInvalidInput.WithErr(errors.New("user ID cannot be empty")) } if id == "nonexistent" { return nil, user.ErrUserNotFound } // Simulate a database error if id == "db_error" { return nil, user.ErrInternal.WithErr(sql.ErrConnDone) } return &user.User{ID: id, Name: "John Doe"}, nil } // Add a helper method to UserError for convenience func (e *UserError) WithErr(err error) *UserError { e.Err = err return e }
errors.Is 또는 errors.As (Go 1.13+)를 사용하여 오류를 확인하고 래핑하는 것을 주목하세요. WithErr 도우미 메서드를 사용하면 API 응답에는 UserError 구조를 유지하면서 내부 로깅을 위해 기본 오류를 쉽게 추가할 수 있습니다.
3. HTTP 핸들러에서 오류 매핑
마지막 조각은 API 핸들러에서 이러한 사용자 정의 오류를 HTTP 응답으로 변환하는 것입니다.
package api import ( "encoding/json" "errors" "log" "net/http" "your_module/your_app/service" "your_module/your_app/user" // Assuming your custom errors are here ) type API struct { userService *service.UserService } func NewAPI(s *service.UserService) *API { return &API{userService: s} } // ErrorResponse defines the structure for API error messages type ErrorResponse struct { Code string `json:"code"` Message string `json:"message"` } func (a *API) GetUserHandler(w http.ResponseWriter, r *http.Request) { userID := r.URL.Query().Get("id") u, err := a.userService.GetUserByID(userID) if err != nil { var userErr *user.UserError if errors.As(err, &userErr) { // It's one of our custom UserErrors log.Printf("Client error: Code=%s, Message=%s, HTTP Status=%d, UnderlyingErr=%v", userErr.Code, userErr.Message, userErr.Status, errors.Unwrap(userErr)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(userErr.Status) json.NewEncoder(w).Encode(ErrorResponse{Code: userErr.Code, Message: userErr.Message}) return } // This handles unexpected errors that aren't our custom UserError type. // We still want to return a generic 500 but log the actual error for debugging. log.Printf("Internal server error: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(ErrorResponse{Code: user.ErrInternal.Code, Message: user.ErrInternal.Message}) return } // Success case w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(u) // Assuming user.User can be marshaled directly }
핸들러에서 errors.As를 사용하여 반환된 오류가 사용자 정의 *user.UserError인지 확인합니다. 이 경우 정확한 HTTP 응답을 구성하기 위해 Status, Code, Message를 추출합니다. 다른 예상치 못한 오류의 경우 http.StatusInternalServerError로 기본 설정하고 내부 디버깅을 위해 전체 오류를 로깅하며 클라이언트에게는 민감한 세부 정보를 노출하지 않습니다.
애플리케이션 확장
이 패턴은 잘 확장됩니다. OrderError, ProductError 등 각 자체 특정 코드 및 HTTP 상태 매핑을 가질 수 있습니다. 중앙 집중식 오류 처리 미들웨어 또는 전용 ErrorMapper 인터페이스는 대규모 애플리케이션의 프로세스를 더욱 간소화할 수 있습니다.
// Example of a generalized error mapper interface // HTTPErrorMapper interface // MapError converts an error into an HTTP status code and a response body. type HTTPErrorMapper interface { MapError(err error) (statusCode int, responseBody interface{}) } // Example usage in middleware // ErrorHandlingMiddleware wraps an HTTP handler with error handling logic. func ErrorHandlingMiddleware(mapper HTTPErrorMapper, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rvr := recover(); rvr != nil { // Handle panics as internal server errors log.Printf("API Panic: %v", rvr) statusCode, response := mapper.MapError(user.ErrInternal.WithErr(fmt.Errorf("%v", rvr))) w.WriteHeader(statusCode) json.NewEncoder(w).Encode(response) return } }() next.ServeHTTP(w, r) }) }
결론
Go API에서 사용자 정의 오류 유형을 생성하고 이를 HTTP 상태 코드로 매핑하는 의도적인 전략을 결합하는 것은 사용자 친화적이고 디버깅 가능한 서비스를 구축하는 강력한 접근 방식입니다. 특정 오류 코드와 메시지를 제공함으로써 API 소비자는 문제에 지능적으로 반응할 수 있으며 백엔드는 명확한 내부 오류 로깅을 통해 이점을 얻습니다. 이 방법은 API를 기능적인 것 이상으로 진정으로 견고하고 전문적으로 만듭니다. 궁극적으로 잘 정의된 사용자 정의 오류는 공감적인 API 디자인의 기초입니다.

