Go 제네릭 도입 후 6개월, 실전 사용 패턴 정리

배경

프로젝트에서 Go 1.23을 쓰면서 제네릭 활용도를 점검했다. 초기엔 인터페이스로도 충분하다고 생각했지만, 반복 코드가 줄어드는 지점이 분명했다.

실전 패턴

1. 슬라이스 유틸리티

가장 빈번하게 사용하는 케이스다. Map, Filter 같은 함수를 타입 안전하게 구현할 수 있다.

func Map[T, U any](items []T, fn func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = fn(item)
    }
    return result
}

// 사용
userIDs := Map(users, func(u User) int64 { return u.ID })

2. Result 타입

Rust의 Result를 참고해서 에러 핸들링 패턴을 개선했다.

type Result[T any] struct {
    value T
    err   error
}

func (r Result[T]) Unwrap() (T, error) {
    return r.value, r.err
}

func NewResult[T any](v T, err error) Result[T] {
    return Result[T]{value: v, err: err}
}

체이닝 로직에서 가독성이 좋아졌다.

3. 캐시 추상화

여러 타입의 캐시를 일관되게 다룰 수 있게 됐다.

type Cache[K comparable, V any] struct {
    data sync.Map
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    val, ok := c.data.Load(key)
    if !ok {
        var zero V
        return zero, false
    }
    return val.(V), true
}

주의점

  • 컴파일 시간이 소폭 증가했다 (체감상 10-15%)
  • 너무 복잡한 타입 제약은 가독성을 해친다
  • 인터페이스로 충분한 경우도 많다

결론

Go 제네릭은 보수적으로 설계되어 있어서, 필요한 곳에만 쓰면 코드 품질이 올라간다. 특히 내부 라이브러리와 유틸리티 함수에서 중복 제거 효과가 컸다.