Dart에서 제네릭(Generic)은 매우 유용한 기능으로, 코드의 재사용성을 높이고 타입 안전성을 보장하는 데 큰 역할을 합니다.
그러나 제네릭은 만능이 아니며, 몇 가지 한계와 제약 조건이 있습니다. 이 글에서는 Dart 제네릭의 한계와 이를 어떻게 관리할 수 있는지에 대해 알아보겠습니다.
1. 제네릭의 한계
1.1 런타임 시 타입 정보 손실 (Type Erasure)
Dart에서는 제네릭 타입 정보가 런타임에 유지되지 않고, 컴파일 시에 제거되는 특성이 있습니다.
이를 타입 소거(Type Erasure)라고 합니다. 이로 인해 런타임에 제네릭 타입에 대한 정보에 접근할 수 없게 됩니다.
void checkType<T>(T item) {
if (item is List<T>) {
print("List of T");
} else {
print("Not a List of T");
}
}
void main() {
checkType<List<int>>([1, 2, 3]); // 출력: Not a List of T
}
위 예제에서 checkType<List<int>>([1, 2, 3]) 호출 시 item is List<T>는 항상 false를 반환합니다. 이유는 컴파일 후 제네릭 타입 정보가 삭제되기 때문입니다.
1.2 제네릭 타입의 인스턴스 생성 불가
Dart에서는 제네릭 타입의 인스턴스를 직접 생성할 수 없습니다. 이는 런타임에 타입 정보가 사라지는 것과 관련이 있습니다.
class Box<T> {
T value;
Box() {
// value = T(); // 오류 발생: 제네릭 타입의 인스턴스를 생성할 수 없음
}
}
위 코드에서 T 타입의 인스턴스를 생성하려고 하면 컴파일 오류가 발생합니다. Dart는 T의 실제 타입을 알 수 없기 때문에 인스턴스를 생성할 수 없습니다.
1.3 제네릭에서 static 멤버 사용 제한
제네릭 타입 매개변수는 클래스의 static 멤버와 함께 사용할 수 없습니다. static 멤버는 클래스 레벨에서 공유되기 때문에, 특정 타입에 종속될 수 없다는 한계가 있습니다.
class Box<T> {
static T staticValue; // 오류 발생: static 멤버에서 제네릭 타입 사용 불가
}
위 코드에서 staticValue를 제네릭 타입으로 선언하려고 하면 컴파일 오류가 발생합니다.
1.4 제네릭 타입의 연산 제한
Dart의 제네릭 타입에서는 기본적인 연산(+, -, *, / 등)을 사용할 수 없습니다.
제네릭 타입이 구체적으로 어떤 타입인지 알 수 없기 때문에 연산을 지원하지 않습니다.
T add<T>(T a, T b) {
return a + b; // 오류 발생: 연산자를 사용할 수 없음
}
위 코드는 T 타입이 어떤 타입이든 해당 타입에 대해 + 연산을 하려고 시도하지만, Dart는 제네릭 타입에 대해 이 연산을 지원하지 않기 때문에 오류가 발생합니다.
2. 제네릭의 제약 조건
2.1 타입 제약 조건 (Type Constraints)
제네릭 타입에 특정 타입만 사용할 수 있도록 제한할 수 있습니다.
extends 키워드를 사용해 제네릭 타입 매개변수가 반드시 특정 클래스나 인터페이스의 서브타입이어야 한다고 명시할 수 있습니다.
class Box<T extends num> {
T value;
Box(this.value);
T add(T other) {
return value + other;
}
}
void main() {
Box<int> intBox = Box(10);
print(intBox.add(5)); // 출력: 15
// Box<String> stringBox = Box("Hello"); // 오류 발생: String은 num의 서브타입이 아님
}
설명
- T extends num으로 제약을 걸면 T는 반드시 num(또는 num의 서브타입)이어야 합니다.
- 이로 인해 Box<int>와 같은 타입은 사용할 수 있지만, Box<String>은 사용할 수 없습니다.
2.2 extends 키워드와 implements의 차이
Dart의 제네릭에서 extends는 클래스나 인터페이스를 상속하거나 구현할 때 사용하는 반면, implements는 특정 인터페이스의 기능을 강제 구현할 때 사용됩니다.
제네릭 타입 제약 조건에서는 extends 키워드만 사용합니다.
class Printer<T extends Printable> {
void printItem(T item) {
item.print();
}
}
abstract class Printable {
void print();
}
class Document implements Printable {
@override
void print() {
print("Printing Document");
}
}
void main() {
Printer<Document> docPrinter = Printer();
docPrinter.printItem(Document());
}
설명
- T extends Printable은 Printable을 구현한 클래스만 Printer의 제네릭 타입으로 사용할 수 있도록 제한합니다.
- Document 클래스는 Printable을 구현했기 때문에 Printer<Document>를 사용할 수 있습니다.
3. 제네릭 사용 시 유의사항
- 타입 안전성 보장: 제네릭을 사용할 때 타입 제약 조건을 명확히 설정하면 타입 안전성을 더욱 강화할 수 있습니다.
- 타입 정보 소실 관리: 런타임에 타입 정보가 소실되는 것을 고려하여, 타입에 의존하지 않는 방법으로 로직을 구현해야 합니다.
- 코드 가독성 유지: 제네릭을 지나치게 남용하면 코드가 복잡해질 수 있으므로, 필요한 경우에만 사용하는 것이 좋습니다.
Dart에서 제네릭은 강력한 도구이지만, 몇 가지 한계와 제약 조건을 가지고 있습니다.
이러한 한계들을 이해하고 제네릭을 적절히 활용하면, 더욱 안전하고 효율적인 코드를 작성할 수 있습니다.
이 글에서 설명한 내용을 참고하여 제네릭을 사용할 때 발생할 수 있는 문제를 미리 대비하시기 바랍니다.
구독!! 공감과 댓글은 저에게 큰 힘이 됩니다.
Starting Google Play App Distribution! "Tester Share" for Recruiting 20 Testers for a Closed Test.
'Dart > Dart Programming language' 카테고리의 다른 글
[고급] Dart 메타프로그래밍/ 어노테이션(Annotations) 사용법 (0) | 2024.09.08 |
---|---|
[고급] Dart 메타프로그래밍/ 리플렉션(Reflection) 기초 (1) | 2024.09.08 |
[고급] Dart제네릭 프로그래밍/ 제네릭 클래스와 함수 작성 방법 - 단계별 예제와 설명 (0) | 2024.09.08 |
[중급] Dart 클래스 심화/연산자 오버로딩과 메소드 체이닝 활용법 (0) | 2024.09.06 |
[중급] Dart 클래스 심화/ 팩토리 생성자와 Singleton 패턴 활용법 (1) | 2024.09.06 |