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스럽지 않다는 의견도 있지만, 실용적으로는 확실히 편하다. 당분간은 기존 코드와 혼재될 것 같지만, 새로 작성하는 유틸리티는 제네릭을 적극 활용할 예정이다.