Go 1.22 제네릭으로 리팩토링하며 배운 것들

배경

사내 API 게이트웨이를 Go로 운영 중인데, 응답 캐싱 로직이 각 엔드포인트마다 중복되어 있었다. Go 1.18에서 제네릭이 추가된 지 2년이 지났고, 이제 충분히 안정화되었다고 판단해 리팩토링을 진행했다.

기존 코드의 문제

func CacheUserResponse(key string, data *User) error {
    bytes, _ := json.Marshal(data)
    return redis.Set(key, bytes, 10*time.Minute)
}

func CacheProductResponse(key string, data *Product) error {
    bytes, _ := json.Marshal(data)
    return redis.Set(key, bytes, 10*time.Minute)
}

타입별로 동일한 로직이 반복되고 있었다. interface{}를 쓸 수도 있지만 타입 안정성이 떨어진다.

제네릭 적용

func Cache[T any](key string, data T, ttl time.Duration) error {
    bytes, err := json.Marshal(data)
    if err != nil {
        return fmt.Errorf("marshal failed: %w", err)
    }
    return redis.Set(key, bytes, ttl)
}

func Get[T any](key string) (T, error) {
    var result T
    bytes, err := redis.Get(key)
    if err != nil {
        return result, err
    }
    err = json.Unmarshal(bytes, &result)
    return result, err
}

사용하는 쪽 코드가 훨씬 간결해졌다.

user, err := cache.Get[*User]("user:123")
product, err := cache.Get[*Product]("product:456")

예상 못한 문제

컴파일 타임이 약 30% 증가했다. 제네릭 함수가 호출되는 각 타입마다 코드가 생성되기 때문이다. 프로젝트 규모가 커지면 이 부분을 고려해야 한다.

또한 제네릭 타입에 메서드를 추가하려면 인터페이스 제약이 필요한데, 이 부분에서 설계가 복잡해질 수 있다.

결론

코드 중복 제거와 타입 안정성 측면에서는 만족스러웠다. 다만 제네릭이 만능은 아니며, 단순한 경우 interface{}나 코드 생성 도구가 더 나을 수 있다. 상황에 맞게 판단해야 한다.