Go 1.23 제네릭을 활용한 공통 응답 래퍼 구현

문제 상황

회사 프로젝트에서 Go로 REST API 서버를 개발하던 중, 모든 엔드포인트가 유사한 응답 구조를 반환하고 있었다. 각 핸들러마다 interface{}로 데이터를 감싸는 코드가 반복되고, 타입 안정성도 떨어졌다.

func GetUser(w http.ResponseWriter, r *http.Request) {
    user := fetchUser()
    response := map[string]interface{}{
        "success": true,
        "data": user,
    }
    json.NewEncoder(w).Encode(response)
}

제네릭 도입

Go 1.18부터 지원되는 제네릭을 활용해 공통 응답 래퍼를 만들었다.

type APIResponse[T any] struct {
    Success bool   `json:"success"`
    Data    T      `json:"data,omitempty"`
    Error   string `json:"error,omitempty"`
}

func WriteJSON[T any](w http.ResponseWriter, status int, data T) error {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    response := APIResponse[T]{
        Success: status < 400,
        Data:    data,
    }
    return json.NewEncoder(w).Encode(response)
}

핸들러 코드가 간결해지고 타입 체크도 컴파일 타임에 가능해졌다.

func GetUser(w http.ResponseWriter, r *http.Request) {
    user := fetchUser()
    WriteJSON(w, http.StatusOK, user)
}

에러 처리 확장

에러 응답도 동일한 구조로 통일했다.

func WriteError(w http.ResponseWriter, status int, message string) error {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    response := APIResponse[any]{
        Success: false,
        Error:   message,
    }
    return json.NewEncoder(w).Encode(response)
}

결과

  • 핸들러 코드 30% 감소
  • 타입 안정성 확보로 런타임 에러 사전 방지
  • 응답 구조 일관성 유지

제네릭이 Go에 늦게 도입되긴 했지만, 실무에서 충분히 유용하게 활용할 수 있다는 걸 확인했다.