본문 바로가기
다트(Dart) 언어 강좌

제네릭을 알아보자

by everythingdev 2024. 7. 27.
반응형

제네릭 유연하고 타입 안전한 프로그래밍의 핵심을 알아보자.

  • 플러터를 시작하기 전 다트(Dart) 언어의 개념에 대해 정리를 해보고자 합니다.
  • 다트(Dart) 언어 개념 정리 포스팅 후 플러터(Flutter) 개념 정리로 넘어갈 예정입니다.
  • 플러터(Flutter) 개념 정리 후 실습이 시작 된다고 보시면 될 것 같습니다.

소개

  • 프로그래밍 세계에서 유연성과 타입 안전성은 매우 중요한 요소입니다.
  • 다트(Dart) 언어는 이 두 가지를 모두 제공하는 강력한 기능인 제네릭을 지원합니다.
  • 제네릭을 사용하면 코드의 재사용성을 높이고 타입 안전성을 유지하면서도 다양한 데이터 타입을 다룰 수 있습니다.
  • 이번 포스팅에서는 다트의 제네릭에 대해 자세히 알아보고, 실제 사용 사례와 함께 그 장점을 살펴보겠습니다.

제네릭이란?

제네릭은 타입을 파라미터로 사용할 수 있게 해주는 프로그래밍 기법입니다. 이를 통해 함수나 클래스가 여러 타입의 데이터를 다룰 수 있게 되어, 코드의 재사용성과 유연성이 크게 향상됩니다. 다트에서 제네릭은 <T>와 같은 형태로 표현되며, 여기서 T는 타입 파라미터를 나타냅니다.

다트에서 제네릭의 기본 사용법

제네릭 함수

  • 다트에서 제네릭 함수를 정의하는 방법은 다음과 같습니다:
T identity<T>(T value) {
  return value;
}
  • 이 함수는 어떤 타입의 값이든 받아서 그대로 반환합니다. 사용 예는 다음과 같습니다:
var result1 = identity<int>(42);    // int 타입
var result2 = identity<String>('Hello');  // String 타입

제네릭 클래스

  • 제네릭은 클래스 정의에도 사용될 수 있습니다:
class Box<T> {
  T value;

  Box(this.value);

  T getValue() {
    return value;
  }
}
  • Box 클래스는 어떤 타입의 값이든 저장할 수 있습니다:
var intBox = Box<int>(42);
var stringBox = Box<String>('Dart');

제네릭의 장점

  1. 코드 재사용성 : 동일한 로직을 여러 타입에 적용할 수 있어 코드 중복을 줄일 수 있습니다.
  2. 타입 안전성 : 컴파일 시점에 타입 체크가 이루어져 런타임 에러를 줄일 수 있습니다.
  3. 성능 향상 : 타입 캐스팅이 줄어들어 성능이 향상됩니다.
  4. 더 명확한 코드 : 의도한 타입을 명시적으로 표현할 수 있어 코드의 가독성이 높아집니다.

고급 제네릭 기능

다중 타입 파라미터

제네릭은 여러 개의 타입 파라미터를 가질 수 있습니다:

class Pair<T, U> {
  T first;
  U second;

  Pair(this.first, this.second);
}

var pair = Pair<int, String>(1, 'one');

제네릭 제약 조건

특정 타입이나 그 서브타입만 허용하도록 제약을 걸 수 있습니다:

class NumberBox<T extends num> {
  T value;

  NumberBox(this.value);

  void printValue() {
    print(value);
  }
}

var intBox = NumberBox<int>(42);    // 가능
var doubleBox = NumberBox<double>(3.14);  // 가능
// var stringBox = NumberBox<String>('Not a number');  // 컴파일 에러

제네릭 메서드

클래스 내부의 메서드도 자체적인 타입 파라미터를 가질 수 있습니다:

class Utilities {
  static T max<T extends Comparable>(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
  }
}

var maxInt = Utilities.max(5, 3);    // 5
var maxString = Utilities.max('apple', 'banana');  // 'banana'

실제 사용 사례

1. 리스트 처리

제네릭을 사용하면 다양한 타입의 리스트를 처리하는 함수를 쉽게 만들 수 있습니다:

void printList<T>(List<T> list) {
  for (var item in list) {
    print(item);
  }
}

var intList = [1, 2, 3];
var stringList = ['a', 'b', 'c'];

printList(intList);
printList(stringList);

2. 캐시 시스템

제네릭을 사용하여 다양한 타입의 데이터를 저장할 수 있는 범용 캐시 시스템을 구현할 수 있습니다:

class Cache<K, V> {
  final Map<K, V> _storage = {};

  void set(K key, V value) {
    _storage[key] = value;
  }

  V? get(K key) {
    return _storage[key];
  }
}

var stringCache = Cache<String, String>();
stringCache.set('name', 'Alice');

var intCache = Cache<int, double>();
intCache.set(1, 3.14);

3. 상태 관리

Flutter 앱에서 제네릭을 사용하여 범용적인 상태 관리 클래스를 만들 수 있습니다:

class StateNotifier<T> {
  T _state;
  final List<Function(T)> _listeners = [];

  StateNotifier(this._state);

  T get state => _state;

  void setState(T newState) {
    _state = newState;
    _notifyListeners();
  }

  void addListener(Function(T) listener) {
    _listeners.add(listener);
  }

  void _notifyListeners() {
    for (var listener in _listeners) {
      listener(_state);
    }
  }
}

// 사용 예
var counterState = StateNotifier<int>(0);
counterState.addListener((state) {
  print('Counter changed: $state');
});

counterState.setState(1);  // 출력: Counter changed: 1

주의사항 및 팁

  1. 타입 추론 : 다트는 대부분의 경우 타입을 추론할 수 있지만, 명시적으로 타입을 지정하면 코드의 의도를 더 명확히 할 수 있습니다.
  2. 동적 타입과의 상호작용 : dynamic 타입과 제네릭을 함께 사용할 때는 주의가 필요합니다. 타입 안전성이 손상될 수 있기 때문입니다.
  3. 제네릭과 null 안전성 : 다트의 null 안전성 기능과 제네릭을 함께 사용할 때는 nullable 타입과 non-nullable 타입을 적절히 사용해야 합니다.
  4. 성능 고려 : 제네릭은 대부분의 경우 성능에 큰 영향을 미치지 않지만, 매우 성능에 민감한 코드에서는 제네릭 사용을 신중히 고려해야 합니다.

결론

  • 다트의 제네릭은 강력하고 유연한 프로그래밍을 가능하게 하는 핵심 기능입니다. 코드의 재사용성을 높이고, 타입 안전성을 유지하면서도 다양한 데이터 타입을 효과적으로 다룰 수 있게 해줍니다.
  • 제네릭을 적절히 활용하면 더 견고하고 유지보수가 쉬운 코드를 작성할 수 있습니다.
  • 다트와 Flutter 개발에서 제네릭의 장점을 충분히 활용하여 더 나은 소프트웨어를 만들어보세요.

맺음말

  • 이번 포스팅에서는 다트의 제네릭에 대해 알아보았습니다.
  • 다음 포스팅에서는 다트의 비동기 프로그래밍에 대해 알아보고자 합니다.
반응형