Go 1.19 제네릭 실전 적용기 - API 응답 래퍼 리팩토링

배경

사내 마이크로서비스 중 하나를 Go로 마이그레이션하면서 API 응답 처리 로직이 반복되는 문제가 있었다. 각 엔드포인트마다 비슷한 구조의 래퍼 함수를 작성하고 있었는데, Go 1.18부터 제네릭이 정식 지원되면서 이를 개선할 기회가 생겼다.

기존 코드

type UserResponse struct {
    Success bool   `json:"success"`
    Data    *User  `json:"data"`
    Error   string `json:"error,omitempty"`
}

type ProductResponse struct {
    Success bool     `json:"success"`
    Data    *Product `json:"data"`
    Error   string   `json:"error,omitempty"`
}

타입마다 동일한 구조의 응답 래퍼를 만들어야 했고, 응답 생성 함수도 각각 작성해야 했다.

제네릭 적용

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

func SuccessResponse[T any](data T) APIResponse[T] {
    return APIResponse[T]{
        Success: true,
        Data:    data,
    }
}

func ErrorResponse[T any](err error) APIResponse[T] {
    return APIResponse[T]{
        Success: false,
        Error:   err.Error(),
    }
}

사용하는 쪽에서는 타입 추론이 잘 작동해서 명시적으로 타입을 지정하지 않아도 됐다.

func GetUser(c *gin.Context) {
    user, err := userService.FindByID(id)
    if err != nil {
        c.JSON(500, ErrorResponse[*User](err))
        return
    }
    c.JSON(200, SuccessResponse(user))
}

페이지네이션 응답

제네릭의 진가는 페이지네이션 응답에서 더 명확하게 드러났다.

type PaginatedResponse[T any] struct {
    Success bool  `json:"success"`
    Data    []T   `json:"data"`
    Total   int64 `json:"total"`
    Page    int   `json:"page"`
    PerPage int   `json:"per_page"`
}

성능 확인

제네릭이 런타임 성능에 영향을 주는지 간단한 벤치마크를 돌려봤다. 인터페이스 기반 구현과 비교했을 때 오버헤드는 거의 없었고, 컴파일 타임이 약간 늘어나는 정도였다.

한계점

메서드에는 타입 파라미터를 추가할 수 없어서 구조체 레벨에서만 제네릭을 활용할 수 있었다. 그래도 반복 코드를 크게 줄이고 타입 안정성을 확보한 것만으로도 충분히 가치가 있었다.

프로덕션에 배포 후 별다른 이슈는 없었고, 새로운 엔드포인트 추가 시 보일러플레이트 코드가 눈에 띄게 줄어들었다.