본문 바로가기
Backend2026년 4월 10일4분 읽기

Go 1.24 제네릭 실전 — 5가지 실무 패턴과 4가지 안티패턴 완전 정리

YS
김영삼
조회 291

핵심 요약

Go 1.18에서 도입된 제네릭은 1.24에서 추론 성능과 타입 파라미터 사용성이 개선됐다. "어디에 쓰면 좋은가"의 실전 기준을 제시한다.

  • 쓰는 곳: 컬렉션 헬퍼, 에러 래퍼, 리포지토리 인터페이스
  • 쓰지 말 곳: 단순 도메인 모델, interface{} 대체만 하는 구간
  • 원칙: "인스턴스화 횟수 ≥ 2, 타입별 로직이 동일"일 때만

실무 패턴 5가지

1) 컬렉션 헬퍼 (Map / Filter / Reduce)

func Map[T, U any](s []T, f func(T) U) []U {
    r := make([]U, len(s))
    for i, v := range s {
        r[i] = f(v)
    }
    return r
}
// 사용
names := Map(users, func(u User) string { return u.Name })

2) Result[T] 타입

type Result[T any] struct {
    Value T
    Err   error
}
func Ok[T any](v T) Result[T]    { return Result[T]{Value: v} }
func Fail[T any](e error) Result[T] { return Result[T]{Err: e} }

3) 옵셔널 Option[T]

type Option[T any] struct {
    value T
    has   bool
}
func Some[T any](v T) Option[T] { return Option[T]{v, true} }
func (o Option[T]) Get() (T, bool) { return o.value, o.has }

4) Repository 인터페이스

type Repository[T any, ID comparable] interface {
    Find(ctx context.Context, id ID) (*T, error)
    Save(ctx context.Context, e *T) error
}

5) 수치 제약(Constraints)

type Number interface {
    ~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
    var s T
    for _, x := range xs { s += x }
    return s
}

안티패턴 4가지

1) interface{} 단순 치환

동적 디스패치가 필요한 곳에 제네릭을 쓰면 컴파일 시간만 늘어난다. 다형성은 interface로, 타입 안정성은 제네릭으로 구분.

2) 깊은 중첩 (Option[Result[Map[K, V]]])

타입 추론이 어려워지고 에러 메시지가 난해해진다. 최대 2단계까지.

3) 도메인 모델에 제네릭 박기

User[T]처럼 비즈니스 엔티티를 제네릭화하지 말 것. 한 타입으로 충분.

4) 함수 1회 호출에 제네릭

호출이 1번뿐이면 그냥 구체 타입을 쓰자. 유지보수성만 떨어진다.

1.24 개선점

  • 타입 추론이 함수 리터럴 인자까지 확장 → Map(xs, func(x) { return x.Name }) 형태 허용
  • 제네릭 타입 별칭(type alias) 정식 지원
  • 컴파일 속도 개선 (대형 코드베이스에서 체감)

자주 묻는 질문

런타임 오버헤드는?

Go 제네릭은 GCShape 기반으로 동일 레이아웃 타입은 하나의 인스턴스를 공유한다. 구체 타입 코드보다 미세하게 느릴 수 있으나 실무상 무시 가능.

interface와 제네릭 중 뭘 먼저?

다형성이면 interface, 타입 안정성이 핵심이면 제네릭. 두 개를 섞어 interface를 제약(constraint)으로 쓰는 패턴이 가장 강력하다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록