async/await 4분 소요

같은 async/await인데 왜 다르게 움직일까요?

핵심 요약

  • async/await 문법만으로는 작업이 언제 시작되고 동시에 진행되는지 알 수 없습니다.
  • 여러 작업의 실패를 어떻게 전달하고 처리할지는 별도의 실행 규칙에 달려 있습니다.
  • 구조적 동시성은 하위 작업의 수명을 이를 관리하는 실행 범위에 묶는 설계입니다.
  • 취소를 요청해도 작업이 즉시 멈추거나 이미 발생한 변경이 원상 복구된다는 보장은 없습니다.

다른 언어의 코드를 읽다가 asyncawait를 만나면 반갑습니다. 문법이 익숙하니 동작도 비슷하겠거니 싶지만, 작업을 시작하고 끝내는 규칙까지 같지는 않습니다. 이 차이를 놓치면 화면을 닫은 뒤에도 다운로드가 계속되거나, 동시에 처리하려던 요청이 하나씩 실행될 수 있습니다.

1. await를 보기 전에: 작업은 언제 시작하나요?

await는 비동기 작업의 결과를 기다리는 지점을 나타냅니다. 결과가 아직 준비되지 않았다면 현재 함수는 진행을 잠시 멈췄다가 나중에 이어갈 수 있습니다.

하지만 이 문법만으로는 작업의 시작 시점까지 알 수 없습니다.

비동기 함수를 호출하면 작업이 바로 진행되기 시작하는 설계가 있습니다. 반대로 호출할 때는 앞으로 실행할 작업을 나타내는 객체만 만들고, 실행은 별도로 맡겨야 하는 설계도 있습니다. 이때 작업을 실행하는 쪽이 런타임이나 스케줄러입니다. 어떤 작업을 언제 진행할지 관리하는 역할을 합니다.

이미지 두 건을 다운로드하는 상황을 생각해보겠습니다. 각각 1초가 걸리고 서로 영향을 주지 않는다고 가정하겠습니다.

두 다운로드를 먼저 시작한 뒤 결과를 차례로 기다리면 전체 시간은 약 1초일 수 있습니다. 첫 번째 다운로드가 끝난 다음 두 번째를 시작하면 약 2초가 걸립니다. 실제 환경에서는 통신 지연과 자원 경쟁 때문에 시간이 달라집니다.

걸리는 시간을 가늠하려면 await가 몇 번 등장하는지보다 두 작업이 언제 시작됐는지를 봐야 합니다.

비동기로 실행한다고 해서 여러 CPU 코어에서 동시에 계산하는 것도 아닙니다. 한 작업이 네트워크 응답을 기다리는 동안 다른 작업을 진행하는 것만으로도 비동기 실행은 성립합니다.

2. 하나가 실패하면 나머지는 어떻게 될까요?

이번에는 화면 하나를 만들려고 프로필과 알림 목록을 함께 가져온다고 해보겠습니다. 프로필 요청은 성공했지만 알림 요청은 실패했습니다.

이때 어떻게 처리할지는 서비스마다 다릅니다.

  • 프로필을 먼저 보여주고 알림 영역에만 오류를 표시할 수 있습니다.
  • 두 결과가 모두 필요하다면 화면 전체를 실패로 처리할 수 있습니다.
  • 한 요청이 실패하는 즉시 아직 진행 중인 다른 요청에 취소를 요청할 수도 있습니다.

async/await 문법만으로는 이 중 어떤 동작을 할지 알 수 없습니다. 여러 작업을 묶는 기능이 어떻게 동작하고 오류를 어떻게 처리하는지도 확인해야 합니다.

특히 오류 전달다른 작업의 중단은 별개의 동작입니다. 호출한 쪽에서 오류를 받았다고 해서 함께 시작한 작업까지 끝났다고 단정할 수 없습니다.

“실패하면 예외를 잡으면 되겠지”라고만 생각하면 이 부분을 놓치기 쉽습니다. 예외를 처리한 뒤에도 어떤 작업이 계속 실행 중인지 확인해야 합니다.

3. 구조적 동시성은 작업의 수명을 관리합니다

일반적인 함수 호출은 흐름을 따라가기 쉽습니다. 함수가 다른 함수를 호출하고, 그 호출이 끝나면 다음 줄로 넘어갑니다.

비동기 작업을 여기저기서 시작하면 이 관계를 파악하기 어려워질 수 있습니다. 화면을 만드는 함수는 이미 끝났는데, 그 안에서 시작한 작업은 계속 실행되는 식입니다.

구조적 동시성은 하위 작업의 수명을 이를 관리하는 실행 범위에 묶는 설계입니다. 작업을 시작한 범위가 끝날 때는 그 범위에 속한 작업이 종료되는 과정도 함께 관리합니다.

예를 들어 검색 화면의 실행 범위 아래에 검색 결과 요청과 추천 검색어 요청을 둘 수 있습니다. 화면이 닫힐 때 이 작업들을 함께 정리하도록 구성하면, 어디에서 작업을 종료할 책임을 지는지 분명해집니다.

다만 구조적 동시성을 지원한다는 설명만으로 모든 동작을 예측할 수는 없습니다. 자식 작업 하나가 실패하면 다른 자식 작업에도 취소를 요청하는지, 여러 오류가 생기면 어떻게 전달하는지는 따로 살펴봐야 합니다.

구조적 동시성을 보면 어느 범위가 어떤 작업을 책임지는지 알 수 있습니다. 그 안에서 실패와 취소를 처리하는 세부 규칙도 함께 확인해야 합니다.

4. 취소를 요청한 뒤에도 할 일이 남습니다

사용자는 취소를 누르면 모든 일이 즉시 멈출 것으로 기대합니다. 하지만 코드에서는 취소를 중단 요청으로 처리할 수 있습니다.

협력적 취소에서는 실행 중인 작업이 취소 요청을 받아 반응합니다. 작업이 취소 여부를 확인하거나 취소를 전달받을 수 있는 대기 지점에 도달해야 중단 절차를 진행합니다. 취소 여부를 확인하지 않고 계산을 오래 이어가면 반응이 늦어질 수 있습니다.

타임아웃이 무엇을 끝내는지도 살펴봐야 합니다. “결과를 기다리는 시간을 넘겼다”는 사실만으로 실제 작업까지 멈췄다고 볼 수는 없습니다. 사용하는 기능이 대기만 끝내는지, 작업에 취소도 요청하는지 확인해야 합니다.

작업을 멈춘 뒤에도 정리할 일이 남습니다. 다운로드 중이었다면 파일을 닫아야 하고, 임시 파일을 만들었다면 삭제할지도 결정해야 합니다. 취소를 요청한 뒤 이런 정리가 끝날 때까지 시간이 걸릴 수 있습니다.

이미 일어난 변경도 살펴봐야 합니다. 파일 일부를 저장한 뒤 취소했다고 해서 저장된 내용이 저절로 사라지지는 않습니다. 원상 복구가 필요하다면 이를 처리하는 코드를 따로 마련해야 합니다.

취소 코드를 읽을 때는 세 단계를 따라가면 좋습니다. 먼저 요청이 어떻게 전달되는지 보고, 작업이 어디서 그 요청에 반응하는지 살펴봅니다. 그 뒤 정리가 끝났다는 사실을 어떻게 확인하는지까지 봐야 합니다.

async/await 코드를 읽을 때는 작업이 시작되는 시점과 실패가 전달되는 경로를 살피고, 취소 후 정리까지 따라가야 합니다. 코드에서 “취소됐습니다”라고 표시하는 시점도 확인할 필요가 있습니다. 요청을 보낸 시점인지, 실제 작업과 정리까지 끝난 시점인지에 따라 뜻이 달라집니다.

async/await 구조적 동시성 비동기 프로그래밍

댓글

    댓글을 불러오는 중...