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{}나 코드 생성 도구가 더 나을 수 있다. 상황에 맞게 판단해야 한다.