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의 간결함을 해치지 않는 선에서 타입 안정성을 높일 수 있어서 만족스럽다. 다만 무분별하게 사용하기보다는, 실제로 반복이 많은 부분에만 선택적으로 적용하는 게 좋겠다는 결론을 내렸다.