Go 1.18 제네릭 도입 후 기존 코드 리팩토링 경험
배경
사내 Go 프로젝트에서 슬라이스 처리를 위한 유틸리티 함수들을 interface{}로 구현해서 사용하고 있었다. Go 1.18이 3월에 릴리즈되면서 제네릭이 정식 지원되기 시작했고, 이번 주에 프로젝트 Go 버전을 업그레이드하면서 리팩토링을 진행했다.
기존 코드의 문제
func Filter(slice []interface{}, fn func(interface{}) bool) []interface{} {
result := []interface{}{}
for _, item := range slice {
if fn(item) {
result = append(result, item)
}
}
return result
}
// 사용 시 타입 변환 필요
users := []User{{ID: 1}, {ID: 2}}
interfaces := make([]interface{}, len(users))
for i, u := range users {
interfaces[i] = u
}
filtered := Filter(interfaces, func(v interface{}) bool {
return v.(User).ID > 0
})
타입 변환이 번거롭고, 런타임에 타입 assertion 실패로 패닉이 발생할 위험이 있었다.
제네릭 적용
func Filter[T any](slice []T, fn func(T) bool) []T {
result := []T{}
for _, item := range slice {
if fn(item) {
result = append(result, item)
}
}
return result
}
// 사용
users := []User{{ID: 1}, {ID: 2}}
filtered := Filter(users, func(u User) bool {
return u.ID > 0
})
타입 변환이 사라지고 컴파일 타임에 타입 체크가 가능해졌다.
Map, Reduce도 구현
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, item := range slice {
result[i] = fn(item)
}
return result
}
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, item := range slice {
result = fn(result, item)
}
return result
}
성능
벤치마크 결과 제네릭 버전이 interface{} 버전보다 약 15% 빨랐다. 리플렉션이나 타입 assertion 오버헤드가 없어진 영향으로 보인다.
소감
제네릭 문법이 Go스럽지 않다는 의견도 있지만, 실용적으로는 확실히 편하다. 당분간은 기존 코드와 혼재될 것 같지만, 새로 작성하는 유틸리티는 제네릭을 적극 활용할 예정이다.