-
golang: 제네릭(Generic)이란?Back-End/Golang 2024. 8. 3. 13:14반응형
제네릭(Generic)은 Go 언어에서 매우 강력한 도구로, 다양한 데이터 타입을 처리하고 코드를 간결하고 유연하게 만드는 데 유용합니다. 이를 통해 코드의 확장성, 안정성, 재사용성을 높일 수 있으며, 다양한 상황에서 효율적으로 사용할 수 있습니다. 제네릭을 잘 활용하면 더 나은 설계를 하고 변화하는 요구사항에 유연하게 대응할 수 있는 코드를 작성할 수 있습니다.
1. 타입 확장성
제네릭을 사용하면 다양한 데이터 타입을 하나의 함수나 구조체로 처리할 수 있습니다. 이는 특정 타입에 제한되지 않고, 여러 타입에 대해 동일한 로직을 사용할 수 있게 합니다.
예시: 숫자 계산 함수
package main import "fmt" // 제네릭 타입 정의 type Number interface { ~int | ~float64 | ~int32 } // 제네릭을 사용한 합계 계산 함수 func Sum[T Number](a, b T) T { return a + b } func main() { fmt.Println(Sum(10, 20)) // int 타입 fmt.Println(Sum(10.5, 20.5)) // float64 타입 fmt.Println(Sum(int32(10), int32(20))) // int32 타입 }
설명:
- 제네릭 타입 T는 Number 인터페이스에 의해 제약됩니다.
- Number 인터페이스는 int, float64, int32와 같은 숫자 타입을 포함하며, 이들을 모두 처리할 수 있습니다.
2. 타입 안정성
제네릭은 컴파일 시점에 타입 검사를 수행하므로, 잘못된 타입 사용으로 인한 오류를 방지할 수 있습니다. 이는 특히 대규모 코드베이스에서 중요한 역할을 합니다.
예시: 타입 검사를 통한 안전한 데이터 처리
package main import "fmt" // 제네릭 스택 구현 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 return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true } func main() { // int 타입 스택 intStack := Stack[int]{} intStack.Push(1) intStack.Push(2) // Pop 연산 item, ok := intStack.Pop() if ok { fmt.Println("Popped item:", item) // Popped item: 2 } // string 타입 스택 stringStack := Stack[string]{} stringStack.Push("hello") stringStack.Push("world") // Pop 연산 strItem, ok := stringStack.Pop() if ok { fmt.Println("Popped item:", strItem) // Popped item: world } }
설명:
- 제네릭 스택은 컴파일 시점에 타입 검사를 수행하여 안전한 데이터 처리를 보장합니다.
- 잘못된 타입의 데이터가 들어오는 것을 방지합니다.
3. 코드 재사용성
제네릭을 사용하면 동일한 로직을 여러 타입에 대해 재사용할 수 있어, 코드 중복을 줄이고 유지보수를 용이하게 합니다.
예시: 데이터 필터링 함수
package main import "fmt" // 제네릭 필터링 함수 func Filter[T any](data []T, predicate func(T) bool) []T { var result []T for _, v := range data { if predicate(v) { result = append(result, v) } } return result } func main() { // 정수 배열에서 짝수만 필터링 numbers := []int{1, 2, 3, 4, 5, 6} evenNumbers := Filter(numbers, func(n int) bool { return n%2 == 0 }) fmt.Println("Even numbers:", evenNumbers) // Even numbers: [2 4 6] // 문자열 배열에서 특정 문자 포함하는 문자열 필터링 strings := []string{"apple", "banana", "cherry"} filteredStrings := Filter(strings, func(s string) bool { return len(s) > 5 }) fmt.Println("Filtered strings:", filteredStrings) // Filtered strings: [banana cherry] }
설명:
- 제네릭 필터링 함수는 다양한 타입의 데이터를 처리할 수 있으며, 코드 재사용성을 극대화합니다.
- 동일한 로직을 여러 타입에 대해 사용할 수 있어 코드 중복을 줄입니다.
4. 유연한 API 설계
제네릭은 API 설계에서 다양한 타입을 유연하게 지원할 수 있도록 도와줍니다. 이를 통해 API 사용자에게 더 많은 선택권을 제공할 수 있습니다.
예시: 데이터베이스 조회 API
package main import ( "fmt" ) // 제네릭 데이터베이스 레코드 type Record[T any] struct { ID int Data T } // 제네릭 데이터베이스 조회 함수 func GetRecords[T any](records []Record[T]) []T { var results []T for _, record := range records { results = append(results, record.Data) } return results } func main() { // int 타입의 데이터베이스 레코드 intRecords := []Record[int]{ {ID: 1, Data: 10}, {ID: 2, Data: 20}, } intResults := GetRecords(intRecords) fmt.Println("Int results:", intResults) // Int results: [10 20] // string 타입의 데이터베이스 레코드 stringRecords := []Record[string]{ {ID: 1, Data: "apple"}, {ID: 2, Data: "banana"}, } stringResults := GetRecords(stringRecords) fmt.Println("String results:", stringResults) // String results: [apple banana] }
설명:
- 제네릭을 사용하여 다양한 타입의 데이터베이스 레코드를 조회할 수 있는 API를 설계할 수 있습니다.
- 사용자에게 더 많은 선택권을 제공하며, API의 유연성을 높입니다.
5. 간결한 코드
제네릭을 사용하면 여러 타입에 대해 동일한 로직을 반복해서 작성할 필요가 없어 코드가 간결해집니다.
예시: 데이터 스왑 함수
package main import "fmt" // 제네릭 스왑 함수 func Swap[T any](a, b T) (T, T) { return b, a } func main() { // int 타입 스왑 x, y := 1, 2 x, y = Swap(x, y) fmt.Println("Swapped:", x, y) // Swapped: 2 1 // string 타입 스왑 s1, s2 := "hello", "world" s1, s2 = Swap(s1, s2) fmt.Println("Swapped:", s1, s2) // Swapped: world hello }
설명:
- 제네릭 스왑 함수는 여러 타입에 대해 동일한 로직을 사용하여 코드 중복을 줄이고 간결한 표현을 제공합니다.
6. 타입 안정성 및 제약 조건 (Constraints)
Go의 제네릭은 제약 조건을 지원하여 특정 인터페이스나 타입에 맞는 데이터만 처리할 수 있도록 제한할 수 있습니다.
예시: 제약 조건을 사용한 연산
package main import ( "fmt" ) // 숫자 타입에 대한 제약 조건 정의 type Number interface { ~int | ~float64 } // 두 숫자를 더하는 제네릭 함수 func Add[T Number](a, b T) T { return a + b } func main() { fmt.Println(Add(10, 20)) // int 타입 fmt.Println(Add(3.5, 2.7)) // float64 타입 // fmt.Println(Add("a", "b")) // 컴파일 오류 발생: string은 Number 인터페이스에 맞지 않음 }
설명:
- 제약 조건을 사용하여 특정 타입에 맞는 연산만 수행하도록 제한할 수 있으며, 타입 안정성을 유지합니다.
7. 유연한 데이터 구조
제네릭을 사용하여 다양한 타입의 데이터를 저장하고 처리할 수 있는 데이터 구조를 쉽게 정의할 수 있습니다.
예시: 제네릭 리스트 구현
package main import "fmt" // 제네릭 리스트 구조체 type List[T any] struct { items []T } // 아이템 추가 func (l *List[T]) Add(item T) { l.items = append(l.items, item) } // 아이템 출력 func (l List[T]) Print() { for _, item := range l.items { fmt.Println(item) } } func main() { // int 타입 리스트 intList := List[int]{} intList.Add(1) intList.Add(2) intList.Print() // 1, 2 // string 타입 리스트 stringList := List[string]{} stringList.Add("hello") stringList.Add("world") stringList.Print() // hello, world }
설명:
- 제네릭 리스트는 다양한 타입의 데이터를 저장할 수 있으며, 타입에 따라 다른 리스트를 생성하여 사용할 수 있습니다.
8. 성능 최적화
제네릭은 특정 타입에 맞는 코드를 컴파일 시점에 생성하므로, 런타임에 발생할 수 있는 불필요한 타입 변환을 줄이고 성능을 최적화할 수 있습니다.
예시: 제네릭 기반의 성능 최적화
package main import "fmt" // 제네릭 평균 계산 함수 func Average[T float64 | int](data []T) float64 { var sum T for _, v := range data { sum += v } return float64(sum) / float64(len(data)) } func main() { ints := []int{1, 2, 3, 4, 5} floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5} fmt.Printf("Average of ints: %.2f\n", Average(ints)) // 3.00 fmt.Printf("Average of floats: %.2f\n", Average(floats)) // 3.30 }
설명:
- 제네릭을 사용하여 특정 타입에 맞는 코드를 생성하므로 런타임에서 불필요한 타입 변환을 피하고 성능을 최적화할 수 있습니다.
반응형'Back-End > Golang' 카테고리의 다른 글
golang: Gin vs Chi (2) 2024.10.17 golang: 루프 변수의 스코프 이슈(Fixing For Loops in Go 1.22) (0) 2024.08.05 golang: Tee 패턴이란? (1) 2024.05.15 golang: 파이프라인(pipeline)이란? (0) 2024.05.15 golang: 배열(Array)과 슬라이스(Slice) (0) 2024.05.11