# 제너릭 클래스

제너릭 클래스는 Dart에서 다양한 타입을 유연하게 처리할 수 있도록 해주는 중요한 기능이다. 제너릭을 사용하면 클래스나 메소드에서 구체적인 타입을 지정하지 않고, 타입 매개변수를 사용하여 다양한 타입을 처리할 수 있다. 이를 통해 코드의 재사용성을 높이고, 타입 안정성을 보장하면서도 더 범용적인 코드를 작성할 수 있게 된다.

#### 제너릭 클래스의 기본 구조

제너릭 클래스는 타입 매개변수를 받아들이는 클래스이다. 이를 통해 특정 타입에 종속되지 않으면서도, 다양한 데이터 타입을 처리할 수 있는 클래스를 만들 수 있다. Dart에서는 클래스명 뒤에 `<T>` 형식으로 제너릭 타입을 선언할 수 있다.

```dart
class Box<T> {
  T value;

  Box(this.value);

  T getValue() {
    return value;
  }
}
```

위 코드에서는 `Box<T>`가 제너릭 클래스를 나타낸다. 여기서 `T`는 타입 매개변수로, 이 클래스는 `T`로 전달된 타입을 사용하여 `value` 필드를 선언하고, 메소드에서 해당 타입을 반환한다.

**타입 매개변수**

타입 매개변수는 클래스나 메소드에서 선언될 수 있으며, 해당 타입을 이용해 다양한 작업을 수행할 수 있다. Dart에서 제너릭 클래스의 타입 매개변수는 `List<int>`, `List<String>` 등과 같이 구체적인 타입으로 대체된다. 이를 통해 코드가 더 유연해진다.

#### 제너릭 클래스의 장점

**1. 코드 재사용성**

제너릭 클래스를 사용하면 하나의 클래스에서 다양한 데이터 타입을 처리할 수 있다. 예를 들어, 위의 `Box` 클래스는 `Box<int>`로 선언하여 정수를 저장할 수도 있고, `Box<String>`으로 선언하여 문자열을 저장할 수도 있다. 이렇게 코드를 중복 없이 재사용할 수 있다.

**2. 타입 안정성**

제너릭 클래스는 컴파일 시점에 타입 검사를 할 수 있도록 도와준다. 이는 타입 안정성을 보장하여, 타입 불일치로 인한 런타임 오류를 예방할 수 있다.

#### 제너릭 클래스에서 타입 매개변수 제한

Dart에서는 제너릭 타입에 특정 타입만 허용되도록 제한할 수 있다. 이를 '타입 제한'이라고 부르며, `extends` 키워드를 사용한다. 예를 들어, 특정 타입이나 그 하위 클래스들만 허용하고 싶을 때 타입 제한을 걸 수 있다.

```dart
class NumberBox<T extends num> {
  T value;

  NumberBox(this.value);

  T add(T other) {
    return value + other;
  }
}
```

위 코드에서는 `T`가 `num` 타입을 상속하는 타입만 올 수 있도록 제한하였다. 즉, `NumberBox<int>`나 `NumberBox<double>`은 허용되지만, `NumberBox<String>`은 컴파일 오류를 발생시킨다.

#### 제너릭 클래스와 타입 안전성

제너릭 클래스를 사용하면 코드에서 타입에 대한 안전성을 확보할 수 있다. 이는 특히 다양한 데이터 타입을 처리해야 하는 클래스에서 유용하다. 예를 들어, 다음과 같이 제너릭 타입을 사용하지 않은 경우와 제너릭을 사용한 경우를 비교해 보자.

제너릭을 사용하지 않은 코드:

```dart
class NonGenericBox {
  var value;

  NonGenericBox(this.value);
}
```

위의 `NonGenericBox` 클래스는 `value` 필드의 타입을 `var`로 지정하였다. 이는 모든 타입을 저장할 수 있게 해주지만, 이후에 해당 값을 사용할 때 어떤 타입인지 알기 어렵다. 이로 인해 런타임에 타입 오류가 발생할 가능성이 높아진다.

제너릭을 사용한 코드:

```dart
class GenericBox<T> {
  T value;

  GenericBox(this.value);
}
```

`GenericBox` 클래스는 제너릭 타입 매개변수 `T`를 사용하여, 생성 시점에 타입을 지정할 수 있다. 이렇게 하면, 클래스 내부에서 타입이 고정되기 때문에 이후 코드에서 해당 타입을 추론할 수 있고, 컴파일 시점에 타입 오류를 잡아낼 수 있다.

#### 타입 매개변수의 다중 사용

제너릭 클래스는 단일 타입 매개변수뿐만 아니라 여러 개의 타입 매개변수를 가질 수 있다. 이를 통해 더욱 복잡한 자료 구조를 유연하게 처리할 수 있다.

```dart
class Pair<K, V> {
  K key;
  V value;

  Pair(this.key, this.value);

  K getKey() {
    return key;
  }

  V getValue() {
    return value;
  }
}
```

위 예제에서 `Pair<K, V>`는 두 개의 타입 매개변수 `K`와 `V`를 받는다. 이를 통해 서로 다른 타입의 데이터를 쌍으로 묶어 관리할 수 있다. `Pair<String, int>`와 같은 방식으로 사용할 수 있다.

이런 다중 타입 매개변수는 특히 Map이나 Tuple과 같은 자료 구조에서 유용하다. 두 개 이상의 서로 다른 데이터 타입을 연관짓거나, 복수의 값을 함께 처리해야 할 때 활용된다.

#### 제너릭과 null safety

Dart의 null safety 시스템과 제너릭은 상호작용하여 더욱 안전한 코드를 작성할 수 있게 해준다. 제너릭 클래스에 nullable 타입을 사용할 경우, 타입 시스템은 해당 값이 null일 수 있음을 알고 처리할 수 있다.

예를 들어, 제너릭 클래스에서 `T?`와 같이 nullable 타입을 허용할 수 있다.

```dart
class NullableBox<T> {
  T? value;

  NullableBox(this.value);

  bool hasValue() {
    return value != null;
  }
}
```

위의 `NullableBox` 클래스는 `T?` 타입을 사용하여, `value`가 null일 수 있음을 명시한다. `hasValue` 메소드에서 null 여부를 체크하여 안전한 처리를 할 수 있다.

#### 제너릭 클래스와 상속

제너릭 클래스는 다른 클래스에서 상속받아 확장할 수 있다. 이때도 제너릭 타입은 그대로 유지되거나, 새롭게 정의할 수 있다. Dart에서는 부모 클래스의 제너릭 타입을 그대로 사용하거나, 자식 클래스에서 타입을 고정할 수도 있다.

```dart
class AnimalBox<T extends Animal> {
  T animal;

  AnimalBox(this.animal);
}

class DogBox extends AnimalBox<Dog> {
  DogBox(Dog dog) : super(dog);

  void bark() {
    animal.bark();
  }
}
```

위 예제에서는 `AnimalBox`라는 제너릭 클래스를 상속받은 `DogBox` 클래스가 있다. `DogBox`는 `T` 타입을 `Dog`으로 고정하여, `Dog` 객체만 처리하도록 제한한다.

#### 제너릭 클래스와 제약 조건

Dart에서 제너릭 타입을 사용할 때는 `extends` 키워드를 사용해 특정 타입으로 제약을 걸 수 있다. 앞서 설명한 것처럼 제약 조건을 설정하면, 제너릭 클래스가 처리할 수 있는 타입을 한정할 수 있고, 특정 메소드나 속성에 접근할 수 있게 된다.

**제약 조건을 사용하는 이유**

1. **타입 안정성**: 제약 조건을 통해, 제너릭 타입이 특정 클래스나 인터페이스를 반드시 상속받게 하여 해당 클래스의 메소드와 속성을 사용할 수 있다.
2. **코드 가독성 향상**: 제약 조건을 사용하면 타입 매개변수가 특정 타입 이상임을 명시적으로 알 수 있어, 코드의 가독성이 높아진다.

**예시: 상속된 메소드 사용**

다음은 `T`가 `Comparable`을 상속하는 경우이다. 이는 `T` 타입의 인스턴스가 `compareTo` 메소드를 사용할 수 있도록 한다.

```dart
class SortedBox<T extends Comparable> {
  T value1;
  T value2;

  SortedBox(this.value1, this.value2);

  T getLesserValue() {
    return value1.compareTo(value2) < 0 ? value1 : value2;
  }
}
```

위 코드에서는 `SortedBox<T extends Comparable>`가 `T`에 대해 `Comparable`을 상속받도록 제약을 설정하였다. 이로 인해 `compareTo` 메소드를 사용해 두 값을 비교할 수 있게 되었으며, 더 작은 값을 반환하는 `getLesserValue` 메소드를 작성할 수 있다.

**상속된 제너릭 클래스에서의 타입 제약**

제너릭 타입에 제약을 걸 때, 상속된 클래스에서도 제약이 유지되거나 변경될 수 있다. 상속된 클래스에서 제약을 확장하거나 더 구체적으로 설정할 수 있다.

```dart
class Vehicle {}

class Car extends Vehicle {}

class Garage<T extends Vehicle> {
  T vehicle;

  Garage(this.vehicle);
}

class CarGarage extends Garage<Car> {
  CarGarage(Car car) : super(car);

  void drive() {
    print("Driving a car");
  }
}
```

위 코드에서는 `Garage` 클래스가 `T`에 대해 `Vehicle` 타입을 상속받도록 제약을 걸었다. 이때 `CarGarage` 클래스는 `Car` 타입을 고정하여, `Car`만을 처리하도록 설계되었다. 이를 통해 `Car`에만 해당되는 메소드(`drive()`)를 구현할 수 있다.

#### 제너릭 클래스와 Iterable

Dart의 제너릭 클래스는 `Iterable`과 같은 컬렉션 타입에서도 강력하게 사용된다. Dart의 `List`, `Set`, `Map`과 같은 컬렉션 클래스들은 모두 제너릭을 기반으로 작성되어 있으며, 각 컬렉션에 저장되는 값의 타입을 제어할 수 있다.

```dart
List<int> intList = [1, 2, 3, 4];
List<String> stringList = ["a", "b", "c"];
```

위 코드에서 `List<int>`는 정수 타입을 저장하는 리스트이고, `List<String>`은 문자열 타입을 저장하는 리스트이다. 이처럼 제너릭을 사용하면 컬렉션에서의 타입 안정성을 보장할 수 있다. Dart의 제너릭 컬렉션 클래스는 내부적으로 효율적인 데이터 처리를 가능하게 한다.

#### 제너릭 클래스와 타입 추론

Dart는 제너릭 클래스를 사용할 때, 타입을 명시적으로 지정하지 않더라도 컴파일러가 타입을 추론할 수 있다. 이는 코드의 간결성을 높이고, 불필요한 타입 선언을 줄여준다.

```dart
var box = Box(123);
```

위의 코드에서는 `Box<int>`로 타입을 명시하지 않았지만, Dart는 자동으로 `Box<int>`로 추론한다. 컴파일러가 매개변수로 전달된 값의 타입을 기반으로 제너릭 타입을 추론하기 때문에, 이처럼 명시적인 타입 선언 없이도 제너릭 클래스를 사용할 수 있다.

#### 상호 참조 제너릭 타입

상호 참조되는 제너릭 타입을 처리할 때는 서로 다른 타입 매개변수를 사용하여 해결할 수 있다. 이 방식은 복잡한 데이터 구조를 설계할 때 유용하다.

```dart
class Node<T> {
  T value;
  Node<T>? next;

  Node(this.value);
}
```

위 코드에서는 `Node<T>`가 자신의 타입 매개변수를 재귀적으로 참조한다. 이는 단일 연결 리스트와 같은 자료 구조를 설계할 때 유용하게 사용할 수 있다.

#### 제너릭 클래스와 타입 계층 구조

제너릭 클래스는 Dart의 타입 계층 구조 내에서 매우 유연하게 작동한다. 예를 들어, `List<Object>`는 모든 타입을 허용하지만, 그보다 구체적인 타입인 `List<int>`는 오직 정수만을 저장할 수 있다. 이러한 타입 계층 구조를 활용하여 다양한 수준의 추상화를 구현할 수 있다.

```dart
List<Object> objectList = [1, "string", 3.0];
List<int> intList = [1, 2, 3];
```

위의 `objectList`는 `Object` 타입을 받아들이므로 다양한 타입의 객체를 포함할 수 있지만, `intList`는 오직 정수만을 허용한다. 제너릭을 사용하면 이러한 차이를 명확하게 구현할 수 있다.
