본문 바로가기
Dart/Dart Programming language

[고급] Dart제네릭 프로그래밍/ 제네릭의 한계와 제약 조건

by Maccrey Coding 2024. 9. 8.
반응형

 

 

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.

 

Tester Share [테스터쉐어] - Google Play 앱

Tester Share로 Google Play 앱 등록을 단순화하세요.

play.google.com

반응형