Go 1.24 제네릭 성능 개선과 실무 적용 사례
배경
사내 API 서버의 캐시 레이어가 interface{}와 타입 단언으로 구현되어 있었다. 런타임 패닉이 가끔 발생했고, 타입 안정성 문제가 계속 지적되었다. Go 1.24에서 제네릭 컴파일 성능이 개선되었다는 소식을 듣고 전환을 결정했다.
기존 코드의 문제
type Cache struct {
store map[string]interface{}
}
func (c *Cache) Get(key string) (interface{}, bool) {
val, ok := c.store[key]
return val, ok
}
// 사용처에서 매번 타입 단언 필요
val, ok := cache.Get("user:123")
user, ok := val.(User) // 런타임 에러 가능성
타입 단언 실수로 인한 패닉이 주 1~2회 발생했고, 각 사용처마다 타입 체크 코드가 중복되었다.
제네릭 적용
type Cache[T any] struct {
store map[string]T
mu sync.RWMutex
}
func (c *Cache[T]) Get(key string) (T, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.store[key]
return val, ok
}
func (c *Cache[T]) Set(key string, val T) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[key] = val
}
사용처에서는 타입을 명시하는 것만으로 타입 안정성 확보가 가능해졌다.
userCache := &Cache[User]{store: make(map[string]User)}
userCache.Set("user:123", User{ID: 123})
user, ok := userCache.Get("user:123") // User 타입 보장
성능 측정
Go 1.24의 개선된 제네릭 인라이닝 덕분에 성능 저하 없이 전환할 수 있었다.
- 컴파일 시간: 기존 대비 5% 증가 (허용 범위)
- 런타임 성능: 벤치마크상 거의 동일
- 바이너리 크기: 약 2% 증가
벤치마크 결과 interface{} 방식과 비교해 오히려 2~3% 빨랐는데, 타입 단언 오버헤드가 제거된 영향으로 보인다.
마이그레이션 전략
한 번에 전체를 바꾸지 않고 서비스별로 점진적으로 적용했다. 먼저 트래픽이 적은 Admin API부터 시작해 문제가 없음을 확인한 후 Main API로 확대했다.
타입별로 캐시 인스턴스를 분리하면서 의도치 않은 타입 혼용이 컴파일 타임에 잡히기 시작했다. 이전에는 런타임에나 발견되던 버그들이었다.
결론
Go 1.24의 제네릭 개선으로 실무에서도 충분히 사용 가능한 수준이 되었다. 타입 안정성 향상과 코드 가독성 개선이 주된 이득이었고, 성능도 문제없었다. 앞으로 새로운 공통 모듈은 제네릭을 기본으로 작성할 예정이다.