# Go 제네릭 완벽 가이드 2026: 타입 파라미터, 제약조건, 면접 대비 > 2026년 기술 면접에서 자주 출제되는 Go 제네릭 질문을 완벽하게 분석합니다. 타입 파라미터, any vs comparable, 틸드 연산자, 커스텀 제약조건, 스레드 세이프 제네릭 캐시 구현까지 다룹니다. - Published: 2026-07-08 - Updated: 2026-07-08 - Author: SharpSkill - Reading time: 5 min --- Go 1.18은 2022년 3월에 제네릭을 도입했으며, 이후 Go 1.21, 1.22 및 그 이후 버전에서 기능이 성숙해졌습니다. 이 글에서는 2026년 기술 면접에서 자주 출제되는 Go 제네릭 질문을 다루며, 기본적인 타입 파라미터부터 고급 제약조건 패턴까지 설명합니다. > **Go 제네릭이란?** > > Go 제네릭은 컴파일 타임 타입 안전성을 유지하면서 모든 데이터 타입과 함께 작동하는 함수와 타입을 작성할 수 있게 합니다. 런타임 타입 어설션을 사용하는 인터페이스와 달리, 제네릭은 컴파일 시간에 타입을 해결하여 더 나은 성능과 조기 오류 감지를 제공합니다. ## Go에서 타입 파라미터 이해하기 타입 파라미터는 Go 제네릭의 기반을 형성합니다. 타입 파라미터는 제네릭 함수나 타입이 사용될 때 지정되는 타입의 플레이스홀더입니다. ```go // generic.go // 타입 파라미터 T를 가진 기본 제네릭 함수 func PrintSlice[T any](items []T) { for _, item := range items { fmt.Println(item) } } // 사용 - 타입 추론이 자동으로 T를 결정 func main() { PrintSlice([]int{1, 2, 3}) // T는 int PrintSlice([]string{"a", "b"}) // T는 string } ``` 대괄호 문법 `[T any]`는 `any`로 제약된 타입 파라미터 `T`를 선언합니다. Go 컴파일러는 인자로부터 구체적인 타입을 추론하므로, 대부분의 경우 명시적 타입 지정이 필요하지 않습니다. ## 면접 빈출 질문: `any`와 `comparable`의 차이점은? 면접에서는 내장 제약조건인 `any`와 `comparable`에 대해 자주 질문합니다. 이 차이점을 이해하면 제네릭에 대한 확실한 지식을 보여줄 수 있습니다. ```go // constraints.go // any: 모든 타입을 허용 (interface{}의 별칭) func Process[T any](value T) T { return value } // comparable: == 와 !=를 지원하는 타입만 func Contains[T comparable](slice []T, target T) bool { for _, v := range slice { if v == target { // 이 비교에는 comparable이 필요 return true } } return false } // 이것은 컴파일됨 Contains([]int{1, 2, 3}, 2) // 이것은 실패: 슬라이스는 comparable이 아님 // Contains([][]int{{1}, {2}}, []int{1}) ``` `comparable` 제약조건은 등호 연산자를 지원하는 타입으로 타입 파라미터를 제한합니다. 슬라이스, 맵, 함수는 언어 수준에서 등호가 정의되어 있지 않으므로 제외됩니다. ## 인터페이스를 사용한 커스텀 타입 제약조건 생성 Go는 인터페이스를 사용하여 커스텀 제약조건을 정의하고, 제네릭 함수가 허용하는 타입을 확장합니다. [Go 명세](https://go.dev/ref/spec#Type_constraints)에서는 제약조건 인터페이스 내에서 타입 요소가 작동하는 방식을 정의합니다. ```go // number.go // 타입 유니온을 사용한 커스텀 제약조건 type Number interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 } // 숫자 타입으로 제약된 제네릭 함수 func Sum[T Number](values []T) T { var total T for _, v := range values { total += v // 모든 Number 타입이 +를 지원하므로 작동 } return total } func main() { fmt.Println(Sum([]int{1, 2, 3})) // 6 fmt.Println(Sum([]float64{1.5, 2.5})) // 4.0 } ``` `|`를 사용한 타입 유니온은 제약조건을 만족하는 정확한 타입을 지정합니다. 이 접근 방식은 런타임 타입 어설션을 피하면서 `any`보다 더 세밀한 제어를 제공합니다. ## 면접 질문: 제약조건에서 `~` 틸드 연산자 설명 제약조건에서 틸드 연산자 `~`는 타입과 해당 기본 타입을 가진 모든 타입과 매칭됩니다. 이 질문은 Go의 타입 시스템에 대한 이해도를 테스트합니다. ```go // underlying.go // ~int는 int와 기본 타입이 int인 모든 타입과 매칭 type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } // 기본 타입이 int인 커스텀 타입 type UserID int type Temperature int64 func Abs[T Signed](value T) T { if value < 0 { return -value } return value } func main() { var id UserID = -42 var temp Temperature = -10 fmt.Println(Abs(id)) // 42 - ~int가 UserID와 매칭되어 작동 fmt.Println(Abs(temp)) // 10 - ~int64가 Temperature와 매칭되어 작동 } ``` `~`가 없으면 제약조건 `int`는 정확히 `int` 타입만 매칭하고, `UserID`와 같은 커스텀 타입과는 매칭되지 않습니다. 틸드는 파생 타입을 포함하도록 제약조건을 확장하여 제네릭 코드를 더 유연하게 만듭니다. ## 제네릭 타입: 구조체와 메서드 제네릭 타입은 함수를 넘어 구조체와 메서드로 확장됩니다. 이 패턴은 데이터 구조 구현에서 자주 사용됩니다. ```go // stack.go // 제네릭 Stack 타입 type Stack[T any] struct { items []T } // Push는 스택에 요소를 추가 func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } // Pop은 최상위 요소를 제거하고 반환 func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T // 타입 T의 제로 값 return zero, false } index := len(s.items) - 1 item := s.items[index] s.items = s.items[:index] return item, true } func main() { intStack := Stack[int]{} intStack.Push(10) intStack.Push(20) val, ok := intStack.Pop() // val=20, ok=true } ``` 제네릭 타입의 메서드는 대괄호로 타입 파라미터를 반복해야 하지만, 새로운 타입 파라미터를 도입할 수는 없습니다. 리시버 `(s *Stack[T])`는 메서드를 `Stack`의 특정 인스턴스화에 바인딩합니다. ## 면접 질문: 왜 메서드는 자체 타입 파라미터를 가질 수 없나요? Go는 메서드에 추가 타입 파라미터를 갖는 것을 금지합니다. 이것은 Java나 C#에서 온 개발자들을 놀라게 하는 설계 결정입니다. [Go 제네릭 제안](https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md)에서는 타입 시스템을 다루기 쉽게 유지하기 위해 이 제한이 존재한다고 설명합니다. ```go // 이것은 유효한 Go 코드가 아님 type Container[T any] struct { value T } // 오류: 메서드는 타입 파라미터를 가질 수 없음 // func (c *Container[T]) Transform[U any](fn func(T) U) U { // return fn(c.value) // } // 유효한 대안: 독립 함수 사용 func Transform[T, U any](c *Container[T], fn func(T) U) U { return fn(c.value) } ``` 해결 방법으로 메서드 대신 여러 타입 파라미터를 가진 최상위 함수를 사용합니다. 이 설계는 메서드 디스패치를 단순하게 유지하고 리시버 타입과 메서드 타입 파라미터 간의 복잡한 상호작용을 피합니다. ## 타입 추론과 제약조건 추론 Go의 타입 추론은 제네릭 함수를 호출할 때 중복성을 줄입니다. 명시적 타입 인자가 필요한 경우를 이해하면 더 깔끔한 코드를 작성할 수 있습니다. ```go // inference.go func Map[T, R any](input []T, transform func(T) R) []R { result := make([]R, len(input)) for i, v := range input { result[i] = transform(v) } return result } func main() { numbers := []int{1, 2, 3} // 타입 추론: T=int, R=string이 인자로부터 추론됨 strings := Map(numbers, func(n int) string { return fmt.Sprintf("%d", n) }) // 복잡한 경우에는 명시적 타입이 필요할 수 있음 // Map[int, string](numbers, converter) } ``` 타입 추론은 함수 인자에서 타입 파라미터로 작동합니다. 컴파일러가 인자만으로 타입을 추론할 수 없을 때, 대괄호 안의 명시적 타입 인자가 모호함을 해결합니다. ## 제약조건 패키지: 표준 라이브러리의 `cmp`와 `slices` Go 1.21은 `Ordered` 제약조건과 비교 함수가 있는 `cmp` 패키지를 추가했습니다. `slices` 패키지는 표준 라이브러리에서 관용적인 제네릭 코드를 보여줍니다. ```go // stdlib.go import ( "cmp" "slices" ) func main() { numbers := []int{3, 1, 4, 1, 5, 9} // slices.Sort는 내부적으로 cmp.Ordered 제약조건 사용 slices.Sort(numbers) // [1, 1, 3, 4, 5, 9] // 정렬된 슬라이스에서 이진 검색 index, found := slices.BinarySearch(numbers, 4) // cmp.Compare는 -1, 0, 또는 1을 반환 result := cmp.Compare(3, 5) // -1 // cmp.Or는 첫 번째 0이 아닌 값을 반환 value := cmp.Or(0, 0, 42, 100) // 42 } ``` [표준 라이브러리 문서](https://pkg.go.dev/cmp)는 이러한 패키지가 순서가 있는 타입에 대한 타입 안전 연산을 위해 제네릭을 어떻게 활용하는지 보여줍니다. 이러한 패키지에 익숙하다는 것은 이론적 이해를 넘어 실용적인 제네릭 지식을 보여줍니다. ## 면접 문제: 제네릭 캐시 구현 일반적인 면접 과제에서는 제네릭하고 스레드 세이프한 캐시 구현을 요청합니다. 이것은 제네릭, [sync 패키지](/technologies/go/interview-questions/sync-primitives)를 사용한 동시성, 그리고 API 설계를 테스트합니다. ```go // cache.go import ( "sync" "time" ) type Cache[K comparable, V any] struct { mu sync.RWMutex items map[K]cacheItem[V] } type cacheItem[V any] struct { value V expiration time.Time } func NewCache[K comparable, V any]() *Cache[K, V] { return &Cache[K, V]{ items: make(map[K]cacheItem[V]), } } func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() c.items[key] = cacheItem[V]{ value: value, expiration: time.Now().Add(ttl), } } func (c *Cache[K, V]) Get(key K) (V, bool) { c.mu.RLock() defer c.mu.RUnlock() item, exists := c.items[key] if !exists || time.Now().After(item.expiration) { var zero V return zero, false } return item.value, true } ``` 이 구현은 키에 `comparable`(맵 요구사항), 값에 `any`를 사용합니다. 별도의 `cacheItem` 구조체는 제네릭 타입이 서로 중첩될 수 있음을 보여줍니다. 스레드 안전성은 `sync.RWMutex`에서 얻으며, 이는 Go [동시성 면접](/technologies/go/interview-questions/concurrency-patterns)에서 다루는 패턴입니다. ## 제로 값과 타입 제약조건 제네릭 코드에서 제로 값을 처리하려면 Go가 파라미터화된 타입의 변수를 어떻게 초기화하는지 이해해야 합니다. ```go // zero.go // 슬라이스가 비어있을 때 제로 값 반환 func First[T any](slice []T) T { if len(slice) == 0 { var zero T // 제로 값: int는 0, string은 "", 포인터는 nil return zero } return slice[0] } // 대안: 모호함을 피하기 위해 포인터 반환 func FirstOrNil[T any](slice []T) *T { if len(slice) == 0 { return nil } return &slice[0] } ``` `var zero T` 패턴은 모든 타입 `T`의 제로 값을 생성합니다. 제로가 유효한 값인 타입(정수의 `0` 등)의 경우, 포인터를 반환하는 변형이 "찾지 못함"과 "제로를 찾음"을 구분합니다. ## 고급: 여러 제약조건 결합 복잡한 제네릭 함수는 타입이 여러 제약조건을 만족해야 할 수 있습니다. Go는 인터페이스 임베딩을 통해 이를 처리합니다. ```go // combined.go // 순서와 문자열 변환 모두를 요구하는 제약조건 type Stringable interface { String() string } type OrderedStringable interface { cmp.Ordered Stringable } // 대안: 인라인으로 여러 제약조건을 가진 타입 파라미터 사용 func PrintSorted[T interface{ cmp.Ordered; fmt.Stringer }](items []T) { slices.Sort(items) for _, item := range items { fmt.Println(item.String()) } } ``` 인터페이스 임베딩은 제약조건을 결합하여, 타입이 모든 임베딩된 인터페이스를 구현하도록 요구합니다. 인라인 문법 `interface{ A; B }`는 명명된 제약조건 타입을 선언하지 않고도 동일한 기능을 제공합니다. ## 결론 - `[T any]`를 사용한 타입 파라미터는 런타임 리플렉션 없이 재사용 가능하고 타입 안전한 코드를 작성할 수 있게 합니다 - `comparable`은 `==`와 `!=`를 지원하는 타입으로 제한하며, 맵 키에 필요합니다 - `~` 틸드는 특정 기본 타입을 가진 타입과 매칭하여 제약조건의 유연성을 확장합니다 - 커스텀 제약조건은 타입 유니온(`int | string`)을 사용하여 허용되는 정확한 타입을 지정합니다 - 메서드는 자체 타입 파라미터를 가질 수 없습니다. 대안으로 최상위 함수를 사용합니다 - 표준 라이브러리 패키지 `cmp`와 `slices`는 관용적인 제네릭 패턴을 보여줍니다 - `var zero T`를 통한 제로 값은 제네릭 함수에서 빈 데이터나 누락된 데이터를 처리합니다 - 스레드 세이프 제네릭 데이터 구조는 제네릭과 sync 프리미티브를 결합합니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/go/go-generics-type-parameters-constraints-interview-questions