🔥 제네릭 타입

618자
7분

Go 언어는 제네릭 함수뿐만 아니라 제네릭 타입도 지원한답니다. 타입 매개변수를 사용해서 타입을 매개변수화할 수 있어요. 이는 제네릭 데이터 구조를 구현할 때 유용하게 쓰일 수 있지요.

다음 예제는 임의의 타입의 값을 저장하는 단일 연결 리스트에 대한 간단한 타입 선언을 보여줍니다.

package main
 
// List는 임의의 타입 T의 값을 저장하는 단일 연결 리스트를 나타냅니다.
type List[T any] struct {
	next *List[T] // 다음 노드를 가리키는 포인터
	val  T        // 현재 노드의 값
}
 
func main() {
}
 
go

위 코드에서 List 타입은 타입 매개변수 T를 받아요. Tany 타입으로 선언되어 있어서, 어떤 타입의 값이라도 저장할 수 있습니다.

List 구조체는 두 개의 필드를 가지고 있어요:

  • next: 같은 타입의 List를 가리키는 포인터입니다. 다음 노드를 가리켜요.
  • val: 타입 T의 값을 저장하는 필드입니다. 현재 노드의 값이 되겠죠.

이렇게 제네릭 타입을 사용하면, 여러 타입에 대해 재사용 가능한 데이터 구조를 만들 수 있답니다. 코드 중복을 줄이고 추상화 수준을 높일 수 있어요.

이 리스트 구현에 몇 가지 기능을 추가해 볼게요:

// Append 메서드는 리스트 끝에 새 값을 추가합니다.
func (l *List[T]) Append(val T) {
	// 리스트가 비어있으면 새 노드를 헤드로 설정
	if l.next == nil {
		l.next = &List[T]{val: val}
		return
	}
 
	// 리스트 끝까지 이동
	current := l
	for current.next != nil {
		current = current.next
	}
 
	// 새 노드를 끝에 추가
	current.next = &List[T]{val: val}
}
 
// GetAt 메서드는 주어진 인덱스의 값을 반환합니다.
func (l *List[T]) GetAt(index int) (T, bool) {
	// 리스트 순회
	current := l.next
	for i := 0; current != nil; i++ {
		if i == index {
			return current.val, true // 인덱스 찾음
		}
		current = current.next
	}
 
	var zero T
	return zero, false // 인덱스 없음
}
 
// RemoveAt 메서드는 주어진 인덱스의 노드를 제거합니다.
func (l *List[T]) RemoveAt(index int) bool {
	// 리스트가 비어있으면 실패
	if l.next == nil {
		return false
	}
 
	// 헤드 제거
	if index == 0 {
		l.next = l.next.next
		return true
	}
 
	// 이전 노드 찾기
	current := l
	for i := 0; current.next != nil; i++ {
		if i == index-1 {
			if current.next.next == nil {
				current.next = nil // 끝 노드 제거
			} else {
				current.next = current.next.next // 중간 노드 제거
			}
			return true
		}
		current = current.next
	}
 
	return false // 인덱스 없음
}
 
// Print 메서드는 리스트의 모든 값을 출력합니다.
func (l *List[T]) Print() {
	current := l.next
	for current != nil {
		fmt.Printf("%v -> ", current.val)
		current = current.next
	}
	fmt.Println("nil")
}
 
func main() {
	// 정수 리스트 생성
	intList := &List[int]{}
 
	// 리스트에 값 추가
	intList.Append(1)
	intList.Append(2)
	intList.Append(3)
 
	// 리스트 출력
	fmt.Print("정수 리스트: ")
	intList.Print()
 
	// 인덱스 1 위치에 있는 값 출력
	fmt.Println(intList.GetAt(1))
	// 인덱스 1 위치에 있는 값 삭제
	intList.RemoveAt(1)
	// 리스트 출력
	intList.Print()
 
	// 문자열 리스트 생성
	strList := &List[string]{}
 
	// 리스트에 값 추가
	strList.Append("Hello")
	strList.Append("World")
 
	// 리스트 출력
	fmt.Print("문자열 리스트: ")
	strList.Print()
}
 
go
  • Append 메서드는 리스트 끝에 새 값을 추가합니다.
    • 리스트가 비어있으면 새 노드를 헤드로 설정하고,
    • 그렇지 않으면 리스트 끝까지 이동한 후 새 노드를 연결하죠.
  • GetAt 메서드는 주어진 인덱스의 값을 반환합니다.
    • 리스트를 순회하며 해당 인덱스의 노드를 찾아요.
    • 인덱스를 찾으면 값과 함께 true를 반환하고,
    • 찾지 못하면 제로값과 false를 반환합니다.
  • RemoveAt 메서드는 주어진 인덱스의 노드를 제거합니다.
    • 리스트가 비어있으면 실패하고,
    • 인덱스가 0이면 헤드를 다음 노드로 업데이트 하죠.
    • 이전 노드를 찾아 연결을 조정하여 노드를 제거해요.
    • 성공하면 true, 실패하면 false를 반환합니다.

이 코드를 실행하면 다음과 같은 출력을 볼 수 있어요:

정수 리스트:
1 -> 2 -> 3 -> nil
2 true
2 -> 3 -> nil
문자열 리스트: Hello -> World -> nil
text

제네릭 타입 List를 사용해서 정수 리스트와 문자열 리스트를 모두 생성하고 사용할 수 있음을 확인할 수 있답니다. 같은 코드로 다양한 타입의 리스트를 다룰 수 있으니 편리하죠? 제네릭을 활용하면 이렇게 코드의 재사용성을 높이고 중복을 줄일 수 있습니다.