비동기(Asynchronous)와 async/await, 그리고 여러개의 await에 대한 비동기 처리(Future.wait/Promise.all)

2021. 1. 29. 01:04Programming/Flutter

반응형

비동기(Asynchronous)에 대해 알아봅시다.

당신은 새로 들어온 직원 A군의 사수를 담당하게 됐습니다. A군이 일을 얼마나 잘 하는지 가늠이 안되는 와중에, 꽤 복잡한 일을 맡기게됐습니다. 당신은 A군이 일을 얼마나 잘 처리했는지 확인하고싶지만, 당장 맡은 일이 바빠서 신경 쓸 겨름이 없어요. 결국 당신은 A군에게 일을 맡겨놓고, '다 끝나면 나한테 말해줘요. 그거 끝나고 뭐 해야하는지 알려줄께요.'라고 말합니다. 그리고 다시 업무로 복귀한 당신. 와! 당신은 지금, A군과 비동기적으로 일하고 있어요!

비동기적으로 일한다는 건 특정 시점으로부터 하나의 작업이 완료될 때까지는 신경쓰고 있지 않다가, 그 작업이 완료되면 처리한 결과를 가지고 계속해서 진행하겠다는 의미입니다. 뭔가 문장이 좀 복잡하죠. 어쩌면 이상한 얘기를 지어내는 것보다 코드를 통해 설명하는게 좀 더 빠를지도 모르겠어요. 예제 코드는 다트(Dart)를 통해서 설명하고 있지만, Future를 Promise로 치환하면 JavaScript의 비동기 처리에 대해서 설명하는 것과 크게 다르지는 않을겁니다.

자, 다음의 예제 코드를 봐주세요. 당신은 Future를 사용해서 A씨를 일하게하고, 자신의 일을 처리합니다. 작업이 완료된 당신은 'Your working is done!'라고 말하고, A씨는 2초동안 일을 처리한 뒤에 'Mr. A said he was done.'라고 말하죠. 영어가 좀 이상한가요? 맞아요, 번역기를 썼기 때문이에요. 그건 그거고, 실행하면 어떤 순서대로 문구가 찍힐지 잘 예상되나요?

Future<String> makeMrA2Working() =>
    Future.delayed(
      Duration(seconds: 2),
      () => 'Mr. A said he was done.',
    );

Future<void> main() {
  print('Let\'s point Mr.A to work.');
  makeMrA2Working().then((result) => print(result));
  print('Your working is done!');
}

맞아요. 출력되는 문구는 다음과 같습니다.

1) Let's point Mr.A to work.
2) Your working is done!
3) Mr. A said he was done.

then을 사용하면 Future를 사용해 비동기적으로 실행한 함수의 처리가 끝난 뒤의 결과를, 콜백함수의 인자로 넘겨받을 수 있습니다. 여기서는 result가 되겠죠. makeMrA2Working의 반환형이 Future<String>이니까, then에 넘긴 콜백함수의 인자 result는 String이 됩니다. 어때요, 간단하죠?

async/await에 대해 알아봅시다.

당신과 A군의 놀라운 활약으로 인해 당신의 회사가 눈부시게 발전했습니다. 뭐, 현실과의 거리감은 잠시 내려놓고, 이제 당신의 팀에 B, C, D, E군이 합류했어요! 이제 막 업무에 익숙해지기 시작한 A군에게 B, C, D, E군의 교육을 일임하기는 좀 애매한 당신. 결국 직접 업무를 분담해주고는, 처리 결과를 종합해서 개개인의 능력치를 파악하고자합니다. 네? 이런건 팀장의 업무가 아니냐구요? 그럼 일단 팀장인걸로 치죠, 뭐. 자, 아래의 코드를 봐주세요.

Future<String> makeWorking(String whom, int potential) =>
    Future.delayed(
      Duration(seconds: potential),
      () => '$whom said he was done.',
    );

Future<void> main() {
  print('Let\'s point Mr.A to work.');
  makeWorking('Mr.A', 2).then((result) => print(result));
  makeWorking('Mr.B', 4).then((result) => print(result));
  makeWorking('Mr.C', 3).then((result) => print(result));
  makeWorking('Mr.D', 1).then((result) => print(result));
  print('Your working is done!');
}

좋아요, 당신은 이제 A, B, C, D군에게 일을 맡겼습니다. 같은 일을 A군은 2초, B군은 4초, C군은 3초, D군은 1초가 걸리겠군요. 이제 일을 맡겼으니 당신은 일을 진행하다가, 모두가 맡긴 업무를 끝내고나면 다음 일을 알려주기만하면 됩니다. 어라? 그런데 위의 코드로는 업무를 시키기만하지, 모두의 업무가 끝낸 뒤에 뭔가 다른 작업을 할 수가 없네요. 코드를 좀 고쳐봅시다.

Future<String> makeWorking(String whom, int potential) =>
    Future.delayed(
      Duration(seconds: potential),
      () => '$whom said he was done.',
    );

Future<void> main() {

  print('Let\'s point Mr.A to work.');
  makeWorking('Mr.A', 2).then((result) => print(result));
  makeWorking('Mr.B', 4).then((result) => print(result));
  makeWorking('Mr.C', 3).then((result) => print(result));
  makeWorking('Mr.D', 1).then((result) => print(result));
  print('Your working is done!');
}
Future<String> makeWorking(String whom, int potential) =>
    Future.delayed(
      Duration(seconds: potential),
      () => '$whom said he was done.',
    );

  bool aIsDone=false,
    bIsDone=false,
    cIsDone=false,
    dIsDone=false,
    bbIsDone=false;

Future<String> watchingWorking() async {
  if (aIsDone && bIsDone && cIsDone && dIsDone) {
    return 'Ok, all done!';
  }
  else {
    return Future.delayed(
      Duration(seconds:1), () => watchingWorking()
    );
  }
}

Future<void> main() {

  print('Let\'s point to work.');
  makeWorking('Mr.A', 2).then((result) {
    aIsDone = true;
    print(result);
  });
  makeWorking('Mr.B', 4).then((result) {
    bIsDone = true;
    print(result);
  });
  makeWorking('Mr.C', 3).then((result) {
    cIsDone = true;
    print(result);
  });
  makeWorking('Mr.D', 1).then((result) {
    dIsDone = true;
    print(result);
  });

  makeWorking('Mr.BB', 1).then((result) {
    bIsDone = true;
    print(result);
  });

  watchingWorking().then((result) => print(result));
}

어째 뭔가가 많이 길어졌네요. 당신은 A, B, C, D군이 일을 다 마치면 각자 전역변수 aIsDone, bIsDone, cIsDone, dIsDone값을 true로 설정해달라고 요청했습니다. 그리고 watchingWorking 함수를 통해서 일하는 와중에 틈틈히, A, B, C, D군이 일을 다 끝냈는지 한번씩 체크하고 있어요. 와, 처음엔 일을 A, B, C, D군에게 맡겨놓고 원래의 일을 하려고 했는데, 결국 1초마다 일이 다 끝났는지 안끝났는지 감시하게 생겼군요.

뭣보다 전역변수라니. 누군가 전역변수를 몰래 바꿔버리면, 분명 A, B, C, D군이 모두 일을 마치지 않았더라도 마치 일이 전부 끝난 것처럼 보일겁니다. 마침 옆 부서의 BB군이 일을 1초만에 마치고 bbIsDone이 아닌, bIsDone을 바꾸고 있네요. 당신은 B군이 4초에 걸쳐 일을 처리하고있는 와중에, 모두가 일이 다 끝난 줄 알고 일단 업무를 정리해버리고맙니다. 엥, 그럼 이제부터 문제가 시작되게되죠. 오타가 이렇게 무섭습니다. 뭐? 누가 코드를 이렇게 짜냐구요? 코드가 길어지면 우연히 변수명이 같아질수도 있고, 오타는 누구나 낼 수 있는거니까요.

좋아요, 그렇다면 이제 async/await 키워드를 사용해서 then과 전역변수를 없애보도록 합시다.

Future<String> makeWorking(String whom, int potential) =>
    Future.delayed(
      Duration(seconds: potential),
      () => '$whom said he was done.',
    );

Future<String> watchingWorking() async {
  return 'Ok, all done!';
}

Future<void> main() async {
  print('Let\'s point to work.');

  print(await makeWorking('Mr.A', 2));
  print(await makeWorking('Mr.B', 4));
  print(await makeWorking('Mr.C', 3));
  print(await makeWorking('Mr.D', 1));

  print(await watchingWorking());
}

뭔가 코드가 확 짧아졌죠? 대충 코드를 봐서 짐작했겠지만, await을 사용하면, 작업이 완료됐을 때 값을 반환합니다. then에 넘겨준 콜백함수의 인자 result가, await을 사용하면 반환되는 것이죠. 즉, await makeWorking('Mr.A', 2)은 2초가 지나서 종료되면, '$whom said he was done.' 문자열을 반환하게됩니다. 어때요, 간단하죠? 자, 코드를 실행해봅시다.

Let's point to work.
Mr.A said he was done.
Mr.B said he was done.
Mr.C said he was done.
Mr.D said he was done.
Ok, all done!

...? 뭔가 이상하지 않나요? 2초 뒤에 Mr.A said he was done.가,
그리고 그로부터 4초 뒤에 Mr.B said he was done.이, 그리고 3초 뒤에 Mr.C said he was done., 마지막으로 1초 뒤에 Mr.D said he was done.가 출력됩니다. 비동기로 일을 시킬 생각이었는데, 어쩌다보니 릴레이가 되어버렸어요. 사실 위의 코드를 then을 사용해서 작성하면 아래와 같은 코드가 되어버립니다.

Future<String> makeWorking(String whom, int potential) =>
    Future.delayed(
      Duration(seconds: potential),
      () => '$whom said he was done.',
    );

Future<String> watchingWorking() async {
  return 'Ok, all done!';
}

Future<void> main() async {
  print('Let\'s point to work.');

  makeWorking('Mr.A', 2).then((result) {
    print(result);
    makeWorking('Mr.B', 4).then((result) {
      print(result);
      makeWorking('Mr.C', 3).then((result) {
        print(result);
        makeWorking('Mr.D', 1).then((result) {
          print(result);
        });
      });
    });
  });

  print(await watchingWorking());
}

await을 호출하면 result의 내용이 리턴되는 것까지는 좋았지만, 이렇게되면 전역변수들을 없앤 의미가 없어져버립니다. A, B, C, D군이 모두 일을 완료하기는 하지만, 제일 느린 B군마저 4초만에 끝낼 수 있는 일을 무려 10초나 걸려서 끝낼 이유는 없으니까요. 여기서 Future.wait()이 등장합니다. 일을 시킬 Future타입의 함수의 배열을 Future.wait() 함수의 인자로 넘겨주면, 모든 배열에 대한 Future 객체가 반환됩니다. 자바스크립트의 Promise.all()과 비슷한 녀석이에요.

Future.wait()의 반환형 역시 완료되지 않은(Uncompleted) Future 객체이기때문에, then을 사용하거나 await을 사용해서 처리가 모두 완료된 뒤의 동작을 정의할 수 있습니다.

Future<void> makeWorking(String whom, int potential) =>
    Future.delayed(
      Duration(seconds: potential),
      () {
        String result = '$whom said he was done.';
        print(result);
        return result;
      },
    );

Future<void> main() async {
  print('Let\'s point to work.');

  var result = await Future.wait([
    makeWorking('Mr.A', 2),
    makeWorking('Mr.B', 4),
    makeWorking('Mr.C', 3),
    makeWorking('Mr.D', 1)
  ]);

  print(result);

  print('Ok, all done!');
}

자, 목표했던 코드를 조금 수정해봤습니다. 마지막에 result를 출력하는 이유는, 처리가 완료된 이후 리턴되는 반환값들이 어떻게 배열에 담기는지 확인하고싶어서입니다. 실행결과는 전역변수를 사용할때랑 동일하지만, 코드는 훨씬 간결해졌습니다.

어, 또 뭔가 눈치챘나요? 맞아요. main함수 마지막에 호출되던 watchingWorking이 없어졌습니다. 사실 진작부터 async/await을 사용하면서부터 필요가 없던 녀석이었는데, 이제와서 없애버렸네요. 이유는 간단합니다. await 키워드를 사용하면 Future로 넘긴 작업이 완료되기 전까지 기다리기때문에, 그 이후에 오는 녀석들은 반드시 await보다 늦게 실행되기 때문이에요. 즉, A, B, C, D군에게 맡긴 일이 모두 완료된 시점에 호출해야 해야 할 일들은, 그 전까지 실행되지 않는다는 얘기입니다. 예시를 드니까 더 애매하게 꼬이는 느낌이네요. 아래의 코드를 봐주세요.

Future<void> main() async {
  print('Let\'s request some HTTP Api');

  var resultA = await Future.wait([
    requestApiA();
    requestApiB();
  ]);

  handleResponseBodyA(resultA);

  val resultB = [];
  requestApiC().then((result) => resultB.add(result));
  requestApiD().then((result) => resultB.add(result));

  print('handleResponseBodyA Done!');
  print('handleResponseBodyB Done!');
}

위에서는 requestApiA()함수와, requestApiB()함수를 Future.wait()으로 호출하고 있어요. requestApiA()requestApiB()는 HTTP API를 서버로 요청하고, 응답을 받는 함수라고 칩시다. 서버로부터 받은 응답을 처리하는 handleResponseBodyA()는 await 키워드 다음에 오게되죠? 여기서 handleResponseBodyA()는 항상 API 응답이 모두 온 다음에 처리된다는 사실을 알 수 있습니다. 반면 동일하게 서버로 요청하는 requestApiC()requestApiD()의 응답을 처리하는 handleResponseBodyB()는, 항상 requestApiC()requestApiD()의 처리가 끝난뒤에 실행될까요? 답은 그럴수도 있고 아닐수도 있습니다.

우리는 글을 위에서부터 아래로 읽는데 익숙합니다. 마찬가지로 코드를 읽을때도, 위에서 아래로 순서대로 읽는데 익숙하죠. async/await을 사용하게되면 코드의 실행 순서가 보장되기때문에, 가독성이 올라간다는 이점도 가지고 있습니다.

얘기가 쓸데없이 길어진데다가, 문단이 두서없이 늘어져버렸네요.
비동기와 비동기를 처리할 때 async/await 키워드를 사용하는 이점에 대해서 살펴봤습니다. 혹시 잘못된 내용이나 추가적인 내용은 댓글로 남겨주세요. 감사합니다. :)

반응형