Dart의 고급 기능: 비동기 프로그래밍과 제네릭

이번 포스팅에서는 Dart 언어의 두 가지 중요한 고급 기능인 비동기 프로그래밍과 제네릭에 대해 알아보겠습니다. 이 기능들은 효율적이고 유연한 코드를 작성하는 데 큰 도움이 됩니다.

동기 프로그래밍 vs 비동기 프로그래밍

동기 프로그래밍

동기 프로그래밍에서는 코드가 순차적으로 실행됩니다. 각 작업은 이전 작업이 완료될 때까지 기다린 후 실행됩니다. 이는 간단하고 직관적이지만, 시간이 오래 걸리는 작업(예: 파일 읽기, 네트워크 요청)이 있을 경우 전체 프로그램의 실행이 blocked 되어 사용자 경험이 저하될 수 있습니다.

예시:

void main() {
  print('작업 1 시작');
  longRunningTask();  // 이 작업이 완료될 때까지 다음 줄로 넘어가지 않습니다.
  print('작업 2 시작');
}

void longRunningTask() {
  // 시간이 오래 걸리는 작업
}

비동기 프로그래밍

비동기 프로그래밍에서는 시간이 오래 걸리는 작업을 “백그라운드”로 보내고, 그 동안 다른 코드를 계속 실행할 수 있습니다. 작업이 완료되면 그 결과를 처리합니다. 이 방식은 프로그램의 반응성을 높이고 자원을 효율적으로 사용할 수 있게 해줍니다.

예시:

void main() async {
  print('작업 1 시작');
  longRunningTask().then((_) => print('긴 작업 완료'));
  print('작업 2 시작');  // longRunningTask가 완료되기 전에 실행됩니다.
}

Future<void> longRunningTask() async {
  // 시간이 오래 걸리는 작업
}

비동기 프로그래밍의 이점:

  1. better performance: 여러 작업을 동시에 처리할 수 있습니다.
  2. improved responsiveness: 사용자 인터페이스가 blocking 되지 않습니다.
  3. efficient resource utilization: CPU와 I/O 리소스를 더 효율적으로 사용할 수 있습니다.

Dart는 Future, async, await 등의 키워드를 제공하여 비동기 프로그래밍을 쉽게 구현할 수 있게 해줍니다. 이제 이러한 개념들을 자세히 살펴보겠습니다.

1. 비동기 프로그래밍

비동기 프로그래밍은 시간이 걸리는 작업(예: 네트워크 요청, 파일 읽기/쓰기)을 처리할 때 프로그램이 멈추지 않고 계속 실행될 수 있게 해줍니다.

Future

Future는 비동기 연산의 결과를 나타내는 객체입니다.

Future<String> fetchUserOrder() {
  return Future.delayed(Duration(seconds: 2), () => 'Large Latte');
}

void main() {
  print('Fetching user order...');
  fetchUserOrder().then((order) => print('Your order is: $order'));
  print('This runs before the order is fetched!');
}

async와 await

asyncawait 키워드를 사용하면 비동기 코드를 더 쉽게 작성할 수 있습니다.

Future<void> main() async {
  print('Fetching user order...');
  String order = await fetchUserOrder();
  print('Your order is: $order');
}

Stream

Stream은 비동기 이벤트의 시퀀스를 나타냅니다.

Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

void main() async {
  Stream<int> stream = countStream(5);
  await for (int number in stream) {
    print(number);
  }
}

2. 제네릭

제네릭을 사용하면 타입에 상관없이 재사용 가능한 코드를 작성할 수 있습니다.

제네릭 클래스

class Box<T> {
  T value;

  Box(this.value);

  T getValue() => value;
  void setValue(T value) => this.value = value;
}

void main() {
  var intBox = Box<int>(42);
  var stringBox = Box<String>('Hello, Dart!');

  print(intBox.getValue());     // 출력: 42
  print(stringBox.getValue());  // 출력: Hello, Dart!
}

제네릭 함수

T first<T>(List<T> list) {
  if (list.isEmpty) throw Exception('List is empty');
  return list[0];
}

void main() {
  var numbers = [1, 2, 3, 4, 5];
  var strings = ['one', 'two', 'three'];

  print(first(numbers));  // 출력: 1
  print(first(strings));  // 출력: one
}

제네릭 제약

특정 타입이나 그 하위 타입만 사용하도록 제한할 수 있습니다.

class Animal {
  void makeSound() => print('Some generic animal sound');
}

class Dog extends Animal {
  @override
  void makeSound() => print('Woof!');
}

void makeAnimalSound<T extends Animal>(T animal) {
  animal.makeSound();
}

void main() {
  var animal = Animal();
  var dog = Dog();

  makeAnimalSound(animal);  // 출력: Some generic animal sound
  makeAnimalSound(dog);     // 출력: Woof!
}

마무리

이번 포스팅에서는 Dart의 고급 기능인 비동기 프로그래밍과 제네릭에 대해 알아보았습니다. 이러한 기능들은 복잡한 애플리케이션을 개발할 때 코드의 효율성과 재사용성을 크게 향상시킵니다.

다음 포스팅에서는 Dart를 사용하여 Flutter로 모바일 앱 개발을 시작하는 방법에 대해 다루겠습니다.

Dart 언어에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요!

관련 리소스

더 자세한 정보를 원하시면 아래의 공식 홈페이지를 참조하세요:

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다