기본 배열 타입 List의 API 문서를 확인하면 실제 타입이 List<E>임을 알 수 있습니다. <...> 각괄호 기호는 제네릭 타입(매개변수화 타입), 즉 정식 타입 매개변수를 가진 타입을 의미합니다. 관례상 대부분의 타입 변수는 E, T, S, K, V처럼 한 글자로 이름을 지정합니다.
제네릭을 사용하는 이유
- 제네릭의 핵심 역할은 타입 안정성을 보장하는 것입니다
- 제네릭을 올바르게 지정하면 더 최적화된 런타임 코드가 생성됩니다;
- 제네릭을 사용하면 코드 중복을 크게 줄일 수 있습니다.
문자열만 저장하는 리스트를 만들고 싶다면 List<String>으로 선언합니다. 이것이 바로 문자열 리스트입니다.
문자열 전용 리스트로 정의한 뒤 문자열이 아닌 타입의 값을 넣으려 하면 오류가 발생합니다.
// 정적 타입 검사 오류
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 컴파일 오류: 42는 문자열이 아닙니다Code language: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
names.add(42); // 컴파일 오류: 42는 문자열이 아닙니다Code language: PHP (php)
코드 중복 줄이기
객체를 캐시하는 추상 인터페이스가 필요하다고 가정해 보겠습니다:
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
Code language: JavaScript (javascript)
나중에 문자열만 저장하는 캐시 인터페이스가 또 필요해지면 별도로 새로 만들어야 합니다:
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
Code language: Dart (dart)
그 뒤 숫자 전용 캐시도 필요하게 되고…… 거의 똑같은 코드를 반복해서 작성해야 합니다.
예를 들어 아래와 같은 코드를 작성하게 됩니다:
abstract class IntCache {
int getByKey(String key);
void setByKey(String key, int value);
}Code language: Dart (dart)
제네릭을 사용하면 타입 매개변수가 있는 범용 인터페이스 한 개만 정의해 이 문제를 한 번에 해결할 수 있습니다:
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}Code language: Dart (dart)
여기서 T는 타입 자리 표시자로, 개발자가 나중에 전달할 임의의 실제 타입을 의미합니다.
구체적인 예시
// 제네릭 추상 캐시 기본 클래스
abstract class Cache<T> {
T? getByKey(String key);
void setByKey(String key, T value);
void remove(String key);
}
// 메모리 캐시 구현 (Map 기반)
class MemoryCache<T> implements Cache<T> {
final Map<String, T> _storage = {};
@override
T? getByKey(String key) {
return _storage[key];
}
@Override
void setByKey(String key, T value) {
_storage[key] = value;
}
@Override
void remove(String key) {
_storage.remove(key);
}
}
void main() {
// 1. 문자열 캐시 예시
final Cache<String> strCache = MemoryCache<String>();
strCache.setByKey("name", "Tom");
print(strCache.getByKey("name")); // Tom
// 2. 정수 int 캐시
final Cache<int> intCache = MemoryCache<int>();
intCache.setByKey("age", 26);
print(intCache.getByKey("age")); // 26
// 3. 사용자 정의 객체 캐시
final Cache<User> userCache = MemoryCache<User>();
userCache.setByKey("u1", User("Jack", 18));
final User? user = userCache.getByKey("u1");
print(user?.name); // Jack
// 삭제 테스트
userCache.remove("u1");
print(userCache.getByKey("u1")); // null
}
// 사용자 정의 엔티티 클래스
class User {
final String name;
final int age;
User(this.name, this.age);
}Code language: Dart (dart)

제네릭 컬렉션 리터럴 사용하기
List, Set, Map 리터럴은 모두 매개변수화 제네릭을 지원합니다:
- List / Set: 왼쪽 대괄호 앞에
<타입>작성 - Map: 왼쪽 중괄호 앞에
<키타입, 값타입>작성
예시:
// 문자열 리스트
var names = <String>['Seth', 'Kathy', 'Lars'];
// 문자열 집합 (중복 자동 제거)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
// 문자열 키, 문자열 값 딕셔너리
var pages = <String, String>{
'index.html': '메인 페이지',
'robots.txt': '크롤러 안내 파일',
'humans.txt': '개발자용 설명 파일',
};
Code language: PHP (php)
실행 예제
void main() {
// 1. List<String> 문자열 리스트: 순서 보장, 중복 허용, 인덱스로 값 조회 가능
var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
print("=== List 리스트 ===");
print(names); // [Seth, Kathy, Lars, Seth] 중복 허용
print(names[0]); // 인덱스로 첫 요소 가져오기: Seth
names.add("Mike"); // 끝에 요소 추가
print("추가 후: $names");
// 2. Set<String> 문자열 집합: 순서 없음, 중복 자동 삭제, 인덱스 없음
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
print("\n=== Set 집합 (중복 자동 제거) ===");
print(uniqueNames); // {Seth, Kathy, Lars} 중복 Seth 자동 삭제
print(uniqueNames.contains("Kathy")); // 존재 여부 확인: true
uniqueNames.remove("Lars");
print("Lars 삭제 후: $uniqueNames");
// 3. Map<String, String> 키-값 딕셔너리: 키는 유일, 키로 값 조회
var pages = <String, String>{
'index.html': '메인 페이지',
'robots.txt': '크롤러 안내 파일',
'humans.txt': '개발자용 설명 파일',
};
print("\n=== Map 딕셔너리 ===");
print(pages);
print(pages['index.html']); // 키로 값 조회: 메인 페이지
pages["about.html"] = "소개 페이지"; // 키-값 추가
print("about.html 추가 후: $pages");
// 자주 사용하는 순회 예시
print("\n=== List 순회 ===");
for (var name in names) {
print("- $name");
}
print("\n=== Map 모든 키-값 순회 ===");
for (var entry in pages.entries) {
print("파일: ${entry.key} → 설명: ${entry.value}");
}
}Code language: Dart (dart)
생성자에 제네릭 매개변수 사용
생성자를 호출할 때 클래스 이름 뒤 각괄호 안에 제네릭 타입을 지정합니다:
var nameSet = Set<String>.of(names);Code language: JavaScript (javascript)
아래 코드는 키 타입 int, 값 타입 View인 딕셔너리를 생성합니다:
var views = SplayTreeMap<int, View>();
Code language: HTML, XML (xml)
import 'dart:collection';
void main() {
var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
var nameSet = Set<String>.of(names);
print("원본 리스트: $names");
print("중복 제거 Set: $nameSet");
// SplayTreeMap은 dart:collection 임포트 필요
var views = SplayTreeMap<int, String>();
views[5] = "설정 페이지";
views[2] = "메인 페이지";
views[9] = "마이 페이지";
views[1] = "시작 화면";
print("\n정렬된 TreeMap:");
for (var e in views.entries) {
print("${e.key} : ${e.value}");
}
}Code language: PHP (php)
제네릭 컬렉션의 런타임 타입
타입 리피케이션(reified)
Dart의 제네릭은 리피케이션(reified)을 지원합니다: 런타임에 제네릭 타입 정보가 완전히 유지됩니다.
런타임에서 컬렉션의 구체적인 제네릭 타입을 직접 검사할 수 있습니다:
void main() {
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true 출력
}Code language: PHP (php)
비교 추가: Java 제네릭은 타입 소거(Type Erasure)를 사용해 런타임에 제네릭 매개변수를 삭제합니다. Java에서는 객체가
List인지만 판단할 수 있고List<String>와List<int>를 구분할 수 없습니다.
* 제네릭 매개변수 타입 제한하기
바운드(bound) 관련 내용입니다. 클래스를 아직 배우지 않았다면 개념만 파악하면 됩니다.
제네릭 정의 시 전달하는 타입이 특정 부모 클래스의 자식 클래스만 오도록 제약할 수 있습니다. 이 제약을 타입 바운드라고 부르며 extends 키워드로 구현합니다.
1 널 불가 타입으로 제한
제네릭이 널 허용 타입이 아닌 타입만 받도록 강제하기 (기본 상한은 널 허용 Object?):
class Foo<T extends Object> {
// 전달하는 T는 널 허용 타입일 수 없음
}
Code language: JavaScript (javascript)
2 지정한 부모 클래스의 자식 클래스로 제한
extends로 기본 클래스를 지정하면 기본 클래스의 모든 멤버 메서드를 직접 호출할 수 있습니다:
class Foo<T extends SomeBaseClass> {
String toString() => "인스턴스 Foo<$T>";
}
class Extender extends SomeBaseClass {}
Code language: JavaScript (javascript)
유효한 타입 인자: 부모 클래스 자체, 모든 자식 클래스
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code language: HTML, XML (xml)
제네릭 매개변수를 생략하면 기본으로 바운드 타입이 사용됩니다:
var foo = Foo();
print(foo); // 출력: 인스턴스 Foo<SomeBaseClass>
Code language: PHP (php)
관계 없는 타입을 전달하면 정적 컴파일 오류가 발생합니다:
var foo = Foo<Object>(); // 컴파일 오류: Object는 SomeBaseClass의 자식 클래스가 아님
Code language: JavaScript (javascript)
* 자기 참조 타입 바운드 (F-bound 유계 제네릭)
제네릭 제약을 걸 때 바운드에 자기 자신의 타입 매개변수를 참조시켜 자기 제약을 만들 수 있으며 이를 F-bound라고 부릅니다.
표준 예시: 비교 가능 인터페이스 Comparable<T>
abstract interface class Comparable<T> {
int compareTo(T o);
}
// T는 Comparable<T>를 구현해야 하며 같은 타입 객체끼리만 비교 가능
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
t1.compareTo(t2) + 1;
class A implements Comparable<A> {
@override
int compareTo(A other) {
// 비교 로직 구현
return 0;
}
}
int res = compareAndOffset(A(), A());
Code language: PHP (php)
T extends Comparable<T> 제약의 의미: T는 동일한 타입 인스턴스끼리만 비교할 수 있다.
* 제네릭 메서드
일반 함수, 클래스 메서드도 제네릭 타입 매개변수를 지원합니다:
T first<T>(List<T> ts) {
// 사전 검증 로직
T tmp = ts[0];
// 후처리 로직
return tmp;
}
Code language: PHP (php)
실제 예시 확인
T first<T>(List<T> ts) {
if (ts.isEmpty) {
throw ArgumentError("리스트는 비어 있을 수 없습니다");
}
T tmp = ts[0];
return tmp;
}
void main() {
List<String> strList = ["apple", "banana", "orange"];
String firstStr = first(strList);
print(firstStr); //apple
List<int> numList = [10, 20, 30];
int firstNum = first(numList);
print(firstNum);//10
}Code language: Dart (dart)
제네릭 매개변수 <T>는 세 곳에 적용됩니다:
- 함수 반환 타입
T - 매개변수 타입
List<T> - 지역 변수 타입
T tmp