Dart

[Dart] 제네릭 (Generics) - 제네릭 함수 & 클래스

Meezzi 2025. 3. 12. 20:43
728x90

1. 제네릭

제네릭은 클래스나 함수에서 다양한 데이터 타입을 유연하게 사용할 수 있도록 해주는 기능입니다.

 

예제 코드를 통해 더 알아볼게요!

int getFirstInt(List<int> items) {
  return items[0];
}

String getFirstString(List<String> items) {
  return items[0];
}

void main() {
  print(getFirstInt([1, 2, 3])); 				// 1
  print(getFirstString(["apple", "banana"])); 	// apple
}

 

getFirstInt 함수와 getFirstString 함수는 모두 매개변수로 받은 리스트의 첫 번째 요소를 반환하는 함수예요.

 

다른 점이라고 한다면, getFirstInt에서는 int타입의 리스트를 매개변수로 받고요.

getFirstString은 String 타입의 리스트를 매개변수로 받아요. 

 

이렇게 데이터의 타입이 다르기 때문에 리스트의 첫 번째 요소를 반환한다는 공통점이 있어도 코드를 중복으로 만들어야 해요.

 

하지만! 제네릭을 사용하면 더 깔끔한 코드를 작성할 수 있습니다!

T getFirst<T>(List<T> items) {
  return items[0];
}

void main() {
  print(getFirst<int>([1, 2, 3]));				// 1
  print(getFirst<String>(["apple", "banana"])); // apple
}

 

getFirstInt 함수와 getFirstString함수가 사라지고 대신 getFirst 함수가 보이네요!

 

그리고 처음 보는 T가 생겨났어요.

 

제네릭이 다양한 데이터 타입을 사용할 수 있다는 것 기억하시나요?

 

그러니까 int와 String에 한정되지 않고 다양한 데이터 타입을 반환하고, 매개변수를 받는 거죠!

이것을 정의하는 게 T입니다.

 

T는 제네릭 변수라고도 하는데요.

이제부터 제네릭의 구성요소에는 뭐가 있는지 알아볼게요.

 


2. 제네릭 타입 변수 (Generic Type Variable)

 

제네릭 변수는 특정 타입을 고정하지 않고, 사용할 때(함수를 호출할 때) 타입을 정할 수 있도록 하는 변수예요.

즉, 클래스나 함수에서 타입을 유연하게 설정하는 변수입니다.

 

위 예제에서는 T가 제네릭 타입 변수에 해당하겠죠?

 

변수 의미 사용 예제
T Type (일반적인 데이터 타입) class Box<T>
K Key (맵의 키) class MyMap<K, V> { }
V Value (맵의 값) class MyMap<K, V> { }
E Element (리스트나 컬렉션 요소) class MyList<E> { }

 

근데 T가 모든 타입을 정의할 수 있는데 왜 K, V, E를 따로 지정할까요? 🤔

 

물론 T를 사용하면 모든 타입을 표현할 수 있는 건 사실이에요!

그런데 굳이 K, V, E 같은 별도의 제네릭 변수를 쓰는 이유는 가독성과 역할을 명확히 하기 위해서입니다.

 

제네릭 변수는 단순히 타입을 일반화하는 것이 아니라 이 타입이 어떤 역할을 하는지도 표현해요.

K, V를 쓰면 그 변수가 각각 Key와 Value 역할을 한다는 것을 직관적으로 알 수 있어요.

 

만약, K, V가 아닌 T, Z 와 같은 변수를 사용한다면 무엇이 Key 역할을 하고, Value 역할을 하는지 알 수 없겠죠?

 

이 변수들은 제네릭 타입을 의미하는 약속된 네이밍이기 때문에 자주 사용되니까 기억해 두시면 좋아요.

 

 

3. 제네릭 함수

 

제네릭 함수는 제네릭을 이용하여 다양한 데이터 타입을 유연하게 처리할 수 있는 함수예요.

T 함수이름<T>(매개변수) {
  return 값;
}

 

맨 앞의 T는 반환할 값을 의미해요.

T라는 건 다양한 데이터 타입을 반환할 수 있다는 뜻이겠죠?

 

<T>는 이 함수가 제네릭임을 선언하는 역할을 해요.

Dart 컴파일러는 T가 제네릭인지, 일반 타입인지 구별할 수 없어요.

그래서 <T>를 선언해야 Dart가 이 함수는 제네릭 함수임을 알 수 있어요.

 

T getFirst<T>(List<T> items) {
  return items[0];
}

void main() {
  print(getFirst<int>([1, 2, 3]));				// 1
  print(getFirst<String>(["apple", "banana"])); // apple
}

 

앞서 설명했던 코드예요.

리스트의 첫 번째 요소를 반환하는 함수이죠.

 

앞서 말했듯 T는 함수의 반환 타입으로 다양한 데이터 타입을 반환할 수 있어요.

 

그리고 List<T> items를 통해 T타입을 가진 List가 매개변수로 온다는 것을 알 수 있죠.

매개변수로 받은 items 리스트의 첫 번째 요소를 반환하는 것이 getFirst 제네릭 함수의 기능입니다.

 

 

4. 제네릭 클래스

제네릭 클래스는 다양한 타입을 처리할 수 있도록 타입을 일반화한 클래스입니다.

 

class 클래스이름<T> {  
  T 변수이름;

  클래스이름(this.변수이름);
}

 

class 클래스이름<T> 는 제네릭 클래스를 선언하는 부분이에요.

이전의 제네릭 함수와 비슷하죠?

 

T 변수 이름; 은 클래스 내부에서 사용할 변수의 타입을 T로 설정한 것입니다.

 

클래스이름(this.변수이름); 은 생성자를 통해 T타입의 변수를 초기화한다는 뜻입니다.

클래스를 사용하기 위해서는 객체를 생성해야 하는데요.

객체를 생성할 때 값을 전달하면 해당 값이 변수에 저장되는 것입니다.

 

 

 

T는 알아봤으니 이제 K와 V에 대해 알아볼게요.

 

K와 V는 Map의 Key-Value  타입을 의미합니다.

class MyMap<K, V> {
  Map<K, V> data = {};

  void add(K key, V value) {
    data[key] = value;
  }

  // 반환 타입이 V?라는 것은 nullable한 Value값을 반환한다는 뜻
  // 매개변수로 K를 받아 해당 key에 해당하는 V 반환
  // 굳이 nullable한 Value값을 반환하는 이유는 
  // 만약 key에 해당하는 value값이 없을 때 null을 반환하기 위함
  V? getValue(K key) {
    return data[key];
  }

  void showAll() {
    print("Map contains: $data");
  }
}

void main() {
  // ageMap 이름을 가진 MyMap 객체 생성
  // Key-String, Value-int로 선언
  MyMap<String, int> ageMap = MyMap<String, int>();
  
  // "Alice" : 25, "Bob" : 30 요소 추가
  ageMap.add("Alice", 25);
  ageMap.add("Bob", 30);
  ageMap.showAll(); 		// Map contains: {Alice: 25, Bob: 30}
  
  print(ageMap.getValue("Alice"));		// 25


  // idMap 이름을 가진 MyMap 객체 생성
  // Key-int, Value-String으로 선언
  MyMap<int, String> idMap = MyMap<int, String>();
  
  // 101 : "Laptop", 102 : "Smartphone" 요소 추가
  idMap.add(101, "Laptop");
  idMap.add(102, "Smartphone");
  idMap.showAll();			// Map contains: {101: Laptop, 102: Smartphone}
  
  print(idMap.getValue(103));		// null
}

 

 

다음은 E 제네릭 변수를 가진 제네릭 함수를 알아볼게요.

E는 Element(요소)를 의미하는 제네릭 타입 변수예요.

class MyList<E> {
  List<E> elements = [];

  void add(E element) {
    elements.add(element);
  }

  // E 타입(리스트의 요소)의 데이터 반환
  E getFirst() {
    return elements.first;
  }

  void showAll() {
    print("List contains: $elements");
  }
}

void main() {

  // numberList라는 이름을 가진 MyList<int> 객체 생성
  MyList<int> numberList = MyList<int>();
  
  numberList.add(10);
  numberList.add(20);	
  numberList.showAll(); 			// List contains: [10, 20]
  
  print(numberList.getFirst());		// 10


  // stringList라는 이름을 가진 MyList<String> 객체 생성
  MyList<String> stringList = MyList<String>();
  stringList.add("Dart");
  stringList.add("Flutter");
  stringList.showAll(); 			// List contains: [Dart, Flutter]
  
  print(stringList.getFirst());		// Dart
}

 

 

 

5. 참고자료

https://dart.dev/language/generics

 

Generics

Learn about generic types in Dart.

dart.dev

 

 

https://dart.dev/effective-dart/design#do-follow-existing-mnemonic-conventions-when-naming-type-parameters

 

Effective Dart: Design

Design consistent, usable libraries.

dart.dev

 

728x90