Go 1.21 제네릭으로 타입 안전한 캐시 레이어 구현
배경
사내 API 서버에서 Redis 캐싱 로직이 여러 곳에 중복되어 있었다. 각 엔드포인트마다 직렬화/역직렬화 코드가 반복되고, 에러 처리도 일관성이 없었다. Go 1.18 이후 제네릭이 안정화되었으니 이를 활용해 타입 안전한 캐시 레이어를 만들기로 했다.
구현
type Cache[T any] struct {
client *redis.Client
prefix string
ttl time.Duration
}
func NewCache[T any](client *redis.Client, prefix string, ttl time.Duration) *Cache[T] {
return &Cache[T]{client: client, prefix: prefix, ttl: ttl}
}
func (c *Cache[T]) Get(ctx context.Context, key string) (*T, error) {
val, err := c.client.Get(ctx, c.prefix+key).Result()
if err == redis.Nil {
return nil, nil
}
if err != nil {
return nil, err
}
var result T
if err := json.Unmarshal([]byte(val), &result); err != nil {
return nil, err
}
return &result, nil
}
func (c *Cache[T]) Set(ctx context.Context, key string, value T) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, c.prefix+key, data, c.ttl).Err()
}
사용은 이렇게 한다.
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
userCache := NewCache[User](redisClient, "user:", 10*time.Minute)
// 캐시 조회
user, err := userCache.Get(ctx, "123")
if err != nil {
return err
}
// 캐시 저장
err = userCache.Set(ctx, "123", User{ID: 123, Name: "John"})
배운 점
제네릭을 쓰니 타입 캐스팅 코드가 사라지고 컴파일 타임에 타입 안전성이 보장되었다. 다만 인터페이스와 달리 제네릭은 모노모픽화되어 각 타입마다 코드가 생성되므로 바이너리 크기는 약간 증가했다.
초기에는 Cache[T comparable] 제약을 사용하려 했으나, 구조체가 comparable하지 않은 경우가 많아 any로 완화했다. 실무에서는 완벽한 추상화보다 실용성이 중요하다는 걸 다시 느꼈다.
캐싱 로직이 한 곳으로 모이면서 모니터링도 쉬워졌다. 다음에는 GetOrSet 패턴과 캐시 워밍까지 추가할 예정이다.