Go 제네릭 실전 적용 후기 - 타입 안정성과 코드 중복 개선

배경

사내 백엔드 API 서버를 Go로 마이그레이션하면서, 반복되는 응답 래핑 로직이 문제였다. 각 타입마다 거의 동일한 코드를 작성하고 있었고, interface{}를 사용하면서 타입 안정성이 떨어졌다.

기존 코드는 이런 식이었다:

func WrapUserResponse(data User) Response {
    return Response{
        Success: true,
        Data: data,
    }
}

func WrapProductResponse(data Product) Response {
    return Response{
        Success: true,
        Data: data,
    }
}

제네릭 적용

Go 1.18 이후 제네릭이 안정화되면서 이번 기회에 도입했다.

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

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

func Error[T any](message string) APIResponse[T] {
    return APIResponse[T]{
        Success: false,
        Error:   message,
    }
}

사용은 간단했다:

func GetUser(c *gin.Context) {
    user := fetchUser()
    c.JSON(200, Success(user))
}

리포지토리 패턴 개선

더 유용했던 건 제네릭 리포지토리 구현이었다. CRUD 로직이 반복되는 부분을 추상화했다.

type Repository[T any] struct {
    db *gorm.DB
}

func (r *Repository[T]) FindByID(id uint) (*T, error) {
    var entity T
    err := r.db.First(&entity, id).Error
    return &entity, err
}

func (r *Repository[T]) Create(entity *T) error {
    return r.db.Create(entity).Error
}

실제 사용:

userRepo := Repository[User]{db: db}
user, err := userRepo.FindByID(1)

트레이드오프

장점은 명확했다. 타입 안정성 확보, 코드 중복 제거, 컴파일 타임 체크. 하지만 몇 가지 제약도 있었다.

  • 제네릭 메서드는 인터페이스에 선언할 수 없었다
  • 복잡한 타입 제약 조건 작성이 생각보다 까다로웠다
  • IDE 자동완성이 가끔 혼란스러웠다

결론

8월 현재 약 3개월간 프로덕션에서 사용 중이다. 특히 API 레이어와 데이터 접근 계층에서 효과적이었다. Go의 간결함을 해치지 않는 선에서 타입 안정성을 높일 수 있어서 만족스럽다. 다만 무분별하게 사용하기보다는, 실제로 반복이 많은 부분에만 선택적으로 적용하는 게 좋겠다는 결론을 내렸다.