Go 1.19 제네릭을 프로덕션에 도입하며 마주친 문제들
배경
사내 API 서버의 공통 유틸리티 패키지를 리팩토링하면서 Go 1.19의 제네릭을 본격적으로 도입했다. 기존에 interface{}로 도배되어 있던 코드를 타입 안전하게 개선하는 것이 목표였다.
문제 1: 타입 추론의 한계
가장 먼저 마주친 문제는 타입 추론이 생각보다 똑똑하지 않다는 점이었다.
func Map[T any, R any](items []T, fn func(T) R) []R {
result := make([]R, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}
// 이런 코드에서 타입을 명시해야 했다
ids := Map[User, int](users, func(u User) int {
return u.ID
})
반환 타입을 추론하지 못해 명시적으로 타입 파라미터를 넘겨야 하는 경우가 많았다. 결국 헬퍼 함수를 만들어 우회했다.
문제 2: 메서드에 타입 파라미터 불가
제네릭 메서드를 정의할 수 없다는 제약도 있었다. 타입 파라미터는 타입 레벨에서만 선언 가능하다.
type Repository[T any] struct {
db *sql.DB
}
// 불가능
// func (r *Repository[T]) Transform[R any](fn func(T) R) R {}
// 타입 레벨에서 모두 선언해야 함
type Repository[T any, R any] struct {
db *sql.DB
}
유연성이 필요한 부분에서는 여전히 interface{}를 쓸 수밖에 없었다.
문제 3: 컴파일 시간 증가
제네릭을 적용한 패키지의 컴파일 시간이 눈에 띄게 늘었다. 약 30% 정도 증가한 것으로 측정됐다. 모노리포 환경에서 CI 시간이 길어지는 것은 부담이었다.
결론
제네릭은 분명 타입 안전성을 높여주지만, Go의 제네릭은 아직 초기 단계라는 인상을 받았다. 당장 모든 코드를 제네릭으로 전환하기보다는 명확히 이득이 있는 부분(컬렉션 유틸, Repository 패턴 등)에만 선택적으로 적용하는 방향으로 결정했다.