Dart 언어 오답노트 : 34. Future과 비동기 프로그래밍

샤워실의 바보·2023년 11월 1일
0
post-thumbnail

이 Dart 프로그램은 Future와 비동기 프로그래밍의 기본적인 개념을 보여줍니다. 프로그램은 두 개의 숫자를 더하는 간단한 작업을 비동기적으로 처리합니다.

void main() {
  // Future 객체를 생성하고 즉시 값을 할당합니다. 
  // 이러한 Future들은 이미 완료된 상태입니다.
  Future<String> name = Future.value('joe');
  Future<int> number = Future.value(1);
  Future<bool> inTrue = Future.value(true);

  // addNumbers 함수를 두 번 호출하여 비동기 계산을 시작합니다.
  addNumbers(10, 20);
  addNumbers(100, 200);
}

void addNumbers(int number1, int number2) {
  print('서버에 계산을 요청합니다. $number1 + $number2');
  print('서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.');

  // 서버 시뮬레이션
  // 2초 뒤에 계산 결과를 출력합니다.
  // Future.delayed를 통해 지정된 시간(2초) 후에 함수를 실행합니다.
  Future.delayed(Duration(seconds: 2), () {
    print('서버가 계산을 완료하였습니다. $number1 + $number2 = ${number1 + number2}');
  });

  // 이 코드는 Future가 완료되기를 기다리지 않고 바로 실행됩니다.
  print('저는 마지막에 있지만 Future보다 빨리 출력됩니다.');
}

이 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다:

서버에 계산을 요청합니다. 10 + 20
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
저는 마지막에 있지만 Future보다 빨리 출력됩니다.
서버에 계산을 요청합니다. 100 + 200
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
저는 마지막에 있지만 Future보다 빨리 출력됩니다.
서버가 계산을 완료하였습니다. 10 + 20 = 30
서버가 계산을 완료하였습니다. 100 + 200 = 300

addNumbers 함수 내의 Future.delayed 호출은 비동기적으로 실행되어 2초 후에 결과를 출력합니다. 이는 메인 함수의 실행 흐름을 방해하지 않고 백그라운드에서 실행되는 것을 의미합니다. 그래서 "저는 마지막에 있지만 Future보다 빨리 출력됩니다."라는 메시지가 먼저 출력되고, 그 후에 비동기적으로 계산 결과가 출력됩니다.

또한, 이 예제에서는 Future.value를 사용하여 즉시 완료된 Future 객체를 생성하는 방법을 보여줍니다. name, number, inTrue 변수에 각각 문자열, 정수, 불리언 값을 갖는 Future 객체가 할당되지만, 이 코드 예제에서는 이를 사용하거나 출력하지는 않습니다.

async-await가 등장한다면?

이 Dart 프로그램은 asyncawait 키워드를 사용하여 비동기 프로그래밍을 다룹니다. async는 비동기 함수를 선언할 때 사용하며, await는 비동기 함수 내에서 Future의 완료를 기다리는 데 사용됩니다. 이는 코드의 실행을 잠시 중단시키고 Future가 완료될 때까지 기다린 다음, 그 결과와 함께 계속 실행합니다.

void main() {
  // Future.value를 사용하여 즉시 완료되는 Future 객체를 생성합니다.
  Future<String> name = Future.value('joe');
  Future<int> number = Future.value(1);
  Future<bool> inTrue = Future.value(true);

  // addNumbers 함수를 비동기적으로 호출합니다.
  // 여기서 두 번의 함수 호출은 동시에 실행되며,
  // 각각의 addNumbers 호출은 내부의 await 표현식에서 2초 동안 대기합니다.
  addNumbers(10, 20);
  addNumbers(100, 200);
}

void addNumbers(int number1, int number2) async {
  print('서버에 $number1 + $number2에 대한 계산을 요청합니다.');
  print('서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.');

  // 서버 시뮬레이션
  // await 키워드를 사용하여 Future가 완료될 때까지 기다립니다.
  await Future.delayed(Duration(seconds: 2), () {
    print('서버가 계산한 결과 : $number1 + $number2 = ${number1 + number2}');
  });

  // await 키워드 덕분에 Future가 완료된 후에 이 메시지가 출력됩니다.
  print('await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.');
}

이 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다:

서버에 10 + 20에 대한 계산을 요청합니다.
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
서버에 100 + 200에 대한 계산을 요청합니다.
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
서버가 계산한 결과 : 10 + 20 = 30
await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.
서버가 계산한 결과 : 100 + 200 = 300
await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.

addNumbers 함수 내에서 await 키워드를 사용하여 Future.delayed의 완료를 기다리므로, "서버가 계산한 결과..." 메시지 출력 후에 "await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다." 메시지가 출력됩니다. 이는 addNumbers 함수의 실행이 Future.delayed 호출이 완료될 때까지 잠시 중단되었다가 이어져서 발생하는 현상입니다.

await 키워드는 리소스의 낭비를 초래할까?

await 키워드를 사용할 때, 비동기 함수 내에서 실행이 일시 중지되고 Future의 완료를 기다립니다. 해당 Future가 완료될 때까지 다른 코드를 실행할 수 없다는 것을 의미하는 것으로 착각할 수도 있습니다. 그러나 이는 Dart의 이벤트 루프와 비동기 프로그래밍 모델 덕분에 실제로는 리소스 낭비가 발생하지 않습니다.

Dart는 싱글 스레드 비동기 프로그래밍 모델을 사용합니다. 이는 코드가 비동기적으로 실행되지만, 한 번에 하나의 작업만 수행되며, 이벤트 루프를 사용하여 비동기 작업을 관리한다는 것을 의미합니다. await 키워드를 사용하여 Future의 완료를 기다리는 동안, Dart의 이벤트 루프는 다른 작업(다른 이벤트 핸들러나 마이크로태스크)을 실행할 수 있습니다.

따라서, await 키워드를 사용하여 비동기 작업을 기다리는 동안 프로그램이 완전히 멈추는 것이 아니라, 다른 작업을 계속 처리할 수 있습니다. 이로 인해 리소스가 효율적으로 사용되며, 프로그램이 더 반응적으로 작동할 수 있습니다.

예를 들어, 사용자 인터페이스를 가진 애플리케이션에서 네트워크 요청을 기다리는 동안에도 사용자의 입력을 처리하고 화면을 업데이트할 수 있습니다. 이는 사용자 경험을 크게 향상시킬 수 있습니다.

결론적으로, await 키워드를 사용하는 것은 비동기 작업을 기다리는 동안 프로그램이 다른 유용한 작업을 수행할 수 있게 해주므로 리소스 낭비가 아닙니다.

앞서 제공된 코드에서 addNumbers 함수는 비동기 함수로 선언되었고, 내부에서 await 키워드를 사용하여 Future.delayed의 결과를 기다리고 있습니다. await 키워드의 작동 방식을 이해하기 위해 코드의 흐름을 자세히 살펴보겠습니다.

void main() {
  // Future - 미래에 받아올 값을 다루는 객체
  // Future.value를 통해 즉시 값을 받는 Future 객체를 생성
  Future<String> name = Future.value('joe');
  Future<int> number = Future.value(1);
  Future<bool> inTrue = Future.value(true);

  // addNumbers 함수 호출
  addNumbers(10, 20);
  addNumbers(100, 200);
}

void addNumbers(int number1, int number2) async {
  print('서버에 $number1 + $number2에 대한 계산을 요청합니다.');
  print('서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.');

  // 서버 시뮬레이션
  // 2초 뒤에 계산 결과를 출력
  // Future.delayed를 통해 지정된 시간(2초) 후에 함수를 실행
  await Future.delayed(Duration(seconds: 2), () {
    print('서버가 계산한 결과 : $number1 + $number2 = ${number1 + number2}');
  });

  print('await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.');
}
  1. main 함수가 실행되고, addNumbers(10, 20)이 호출됩니다.
  2. addNumbers 함수 내에서 첫 번째와 두 번째 print 함수가 실행되어 계산 요청 메시지를 출력합니다.
  3. await Future.delayed(Duration(seconds: 2), () { ... }) 코드를 만나게 됩니다. 이 때, 2초 동안의 지연이 시작되고, 이벤트 루프는 다른 작업을 실행할 수 있게 됩니다.
  4. main 함수의 실행 흐름이 다시 시작되고, addNumbers(100, 200)이 호출됩니다.
  5. 위의 2-3단계가 addNumbers(100, 200)에 대해서도 동일하게 실행됩니다.
  6. 2초의 지연 시간이 끝나고, addNumbers(10, 20)에 대한 지연이 완료되어 결과가 출력됩니다.
  7. 이어서 addNumbers(10, 20)의 마지막 print 함수가 실행되어 메시지를 출력합니다.
  8. addNumbers(100, 200)에 대한 지연 시간도 끝나고 결과 및 메시지가 출력됩니다.

이 예에서 볼 수 있듯이, await 키워드를 사용하더라도 프로그램의 다른 부분이 계속 실행될 수 있습니다. await는 현재 실행 중인 함수의 실행을 일시 중지하고 결과를 기다리지만, 이벤트 루프는 계속해서 다른 작업을 처리할 수 있습니다. 이는 리소스를 효율적으로 사용하며, 사용자 경험을 개선하는데 도움을 줍니다.

아래에 main 함수에 asyncawait를 사용한 경우와 사용하지 않은 경우의 차이점을 설명하겠습니다.

main 함수에 asyncawait를 사용한 경우:

void main() async {
  // Future - 미래에 받아올 값을 다루는 객체
  // Future.value를 통해 즉시 값을 받는 Future 객체를 생성
  Future<String> name = Future.value('joe');
  Future<int> number = Future.value(1);
  Future<bool> inTrue = Future.value(true);

  // addNumbers 함수 호출
  await addNumbers(10, 20);
  await addNumbers(100, 200);
}

Future<void> addNumbers(int number1, int number2) async {
  print('서버에 $number1 + $number2에 대한 계산을 요청합니다.');
  print('서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.');

  // 서버 시뮬레이션
  // 2초 뒤에 계산 결과를 출력
  // Future.delayed를 통해 지정된 시간(2초) 후에 함수를 실행
  await Future.delayed(Duration(seconds: 2), () {
    print('서버가 계산한 결과 : $number1 + $number2 = ${number1 + number2}');
  });

  print('await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.');
}

핵심 내용 위주로 나누어 설명해 보겠습니다.

void main() async {
  await addNumbers(10, 20);
  await addNumbers(100, 200);
}
  • main 함수가 비동기 함수로 선언되어 있습니다. 이는 main 함수 내부에서 await 키워드를 사용할 수 있다는 것을 의미합니다.
  • await addNumbers(10, 20)을 호출하면, addNumbers 함수의 실행이 완료될 때까지 기다립니다. 즉, addNumbers(10, 20)이 완전히 실행되고 나서야 다음 줄인 await addNumbers(100, 200)이 실행됩니다.
  • 이렇게 하면 두 함수 호출 사이에 명확한 순서가 생기고, 첫 번째 함수가 완전히 실행된 후에만 두 번째 함수가 실행됩니다.

main 함수에 asyncawait를 사용하지 않은 경우:

void main() {
  addNumbers(10, 20);
  addNumbers(100, 200);
}
  • main 함수는 비동기 함수가 아닙니다.
  • addNumbers(10, 20)addNumbers(100, 200)은 거의 동시에 호출됩니다.
  • addNumbers 호출은 비동기 함수이므로, Dart는 이 함수들을 비동기적으로 실행하고, main 함수의 실행을 계속 진행합니다. 결과적으로 addNumbers(10, 20)의 실행이 완료되기 전에 addNumbers(100, 200)이 실행될 수 있습니다.
  • 이렇게 하면 두 함수 호출 사이에 명확한 순서가 없고, 두 함수는 비동기적으로 실행됩니다.
  • asyncawait를 사용하면 코드의 실행 순서를 보다 명확하게 제어할 수 있으며, 비동기 함수의 결과를 기다릴 수 있습니다. 이는 코드의 가독성과 이해하기 쉬움을 증가시킵니다.
  • 반면, asyncawait를 사용하지 않으면, 코드의 실행이 비동기적으로 이루어지며, 이로 인해 특정 코드의 실행 순서를 예측하기 어려울 수 있습니다.
  • console 출력값
서버에 10 + 20에 대한 계산을 요청합니다.
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
서버가 계산한 결과 : 10 + 20 = 30
await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.
서버에 100 + 200에 대한 계산을 요청합니다.
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
서버가 계산한 결과 : 100 + 200 = 300
await 키워드가 있으므로 이제 저는 Future보다 늦게 출력됩니다.

addNumbers 함수가 반환값을 가진다면?

void main() async {
  // Future - 미래에 받아올 값을 다루는 객체
  // Future.value를 통해 즉시 값을 받는 Future 객체를 생성
  Future<String> name = Future.value('joe');
  Future<int> number = Future.value(1);
  Future<bool> inTrue = Future.value(true);

  // addNumbers 함수 호출
  final result1 = await addNumbers(10, 20);
  final result2 = await addNumbers(100, 200);
}

Future<int> addNumbers(int number1, int number2) async {
  print('서버에 $number1 + $number2에 대한 계산을 요청합니다.');
  print('서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.');

  // 서버 시뮬레이션
  // 2초 뒤에 계산 결과를 출력
  // Future.delayed를 통해 지정된 시간(2초) 후에 함수를 실행
  await Future.delayed(Duration(seconds: 2), () {
    print('서버가 계산한 결과 : $number1 + $number2 = ${number1 + number2}');
  });
  
  return number1 + number2;
}

이 코드의 주요 변경 사항은 addNumbers 함수에서 결과 값을 반환하는 부분입니다. 이전에는 단순히 출력만 했지만, 이제는 Future.delayedawait하고, 계산 결과를 반환하는 구조로 바뀌었습니다.

여기 바뀐 부분을 설명합니다:

  1. addNumbers 함수의 반환 타입:
    함수 선언부에 Future<int>가 추가되었습니다. 이는 이 함수가 비동기적으로 int 값을 반환할 것임을 나타냅니다.

  2. 결과 반환:
    Future.delayed 후에 이전에는 결과를 콘솔에 출력만 했었지만, 이제는 return number1 + number2;를 통해 실제 계산 결과를 호출자에게 반환합니다.

  3. main 함수에서의 결과 사용:
    main 함수에서는 이제 addNumbers의 반환 값을 변수에 저장하고 있습니다. 이는 각각 result1, result2라는 이름의 두 변수에 저장되며, 함수가 반환하는 Future<int>의 결과 값을 담습니다.

이러한 변경을 통해, 이제 addNumbers 함수는 계산 결과를 호출자에게 반환하고, 이 호출자는 해당 결과를 사용할 수 있게 됩니다. 함수가 async이기 때문에, addNumbers는 비동기적으로 실행되고, awaitmain 함수 내에서 그 결과가 도착할 때까지 실행을 일시 정지합니다.

await를 사용하는 이유는 비동기 함수의 결과를 동기적인 방식으로 받아오고자 할 때 사용합니다. 이는 함수가 완전히 완료될 때까지 기다리게 만들며, 이 때문에 await를 사용한 줄 다음의 코드는 해당 Future가 완료될 때까지 실행되지 않습니다.

서버에 10 + 20에 대한 계산을 요청합니다.
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
서버가 계산한 결과 : 10 + 20 = 30
서버에 100 + 200에 대한 계산을 요청합니다.
서버에서 계산을 시작합니다. 2초 후에 응답하겠습니다.
서버가 계산한 결과 : 100 + 200 = 300
profile
공부하는 개발자

0개의 댓글