ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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
    }

    설명:

    • 제네릭을 사용하여 특정 타입에 맞는 코드를 생성하므로 런타임에서 불필요한 타입 변환을 피하고 성능을 최적화할 수 있습니다.

     

    반응형
Designed by Tistory.