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

배경

회사 마이크로서비스 중 하나를 Go로 재작성하는 프로젝트를 진행 중이었다. API 응답 구조가 일관되지 않아 클라이언트 측에서 파싱 오류가 자주 발생했고, 이를 개선하기 위해 제네릭을 활용한 응답 래퍼를 도입했다.

기존 코드의 문제

기존에는 interface{}를 사용해 응답 데이터를 감싸고 있었다.

type Response struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data"`
    Error   string      `json:"error,omitempty"`
}

타입 안정성이 없어서 런타임 에러가 발생하기 쉬웠고, 테스트 코드 작성 시에도 타입 단언을 반복해야 했다.

제네릭 적용

Go 1.21 환경에서 제네릭을 활용해 리팩토링했다.

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

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

func NewErrorResponse[T any](err string) ApiResponse[T] {
    return ApiResponse[T]{
        Success: false,
        Error:   err,
    }
}

핸들러에서는 명확한 타입으로 응답을 생성할 수 있게 되었다.

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

개선 효과

  • 컴파일 타임에 타입 오류 검출
  • 테스트 코드에서 타입 단언 제거
  • IDE 자동완성 지원 향상
  • 코드 가독성 개선

주의사항

제네릭 함수는 각 타입별로 별도 인스턴스가 생성되므로 바이너리 크기가 약간 증가했다. 하지만 타입 안정성 확보라는 이점이 더 크다고 판단했다.

중첩된 제네릭 구조는 가독성을 해칠 수 있어 최대 2단계까지만 사용하기로 팀 컨벤션을 정했다.