−비동기 작업의 취소(cancellation)는 호출자가 더 이상 필요하지 않은 작업에 중단 의사를 전달하고, 그 작업이 종료되거나 결과가 더 사용되지 않도록 수명을 조정하는 일이다. 화면을 닫았는데도 이미지 다운로드가 계속되거나, 새 검색을 시작했는데 옛 검색 결과가 나중에 화면을 덮는 상황에서 필요해진다.
이때 중단하려는 대상에는 서로 다른 층이 있다. 결과를 기다리는 코드를 끝내는 일, 계산이나 통신을 실제로 멈추는 일, 이미 끝난 작업의 결과를 적용하지 않는 일은 각각 다른 동작이다. 어느 층을 취소하는지 API의 계약을 읽어야 한다.
작업을 시작한 뒤에는 호출자가 그 내부의 모든 실행을 직접 통제하기 어렵다. 네트워크 요청은 원격 서버로 넘어가고, 파일 읽기는 운영체제의 입출력과 연결되며, 계산은 이벤트 루프를 점유할 수 있다. 그래서 많은 API는 취소 신호를 전달하고 작업이 이를 관찰해 정리하도록 하는 협력적 취소를 사용한다.
취소는 작업 수명과 동시성 제어가 만나는 지점이기도 하다. 새 검색이 시작됐다는 사건과 옛 검색의 완료가 어떤 순서로 처리되는지에 따라 화면이 달라질 수 있다. 작업을 멈추려는 시도와 화면을 바꿀 권한을 관리하는 처리를 함께 이해해야 이 문제의 구조가 보인다.
실행을 끊는 일에서 중단을 요청하는 일로 (2009년의 회고)
Mike Liddell은 2009년 .NET 4 취소 프레임워크를 소개하면서 당시의 두 접근을 비교했다. 실행 중인 코드를 강제로 끊으면 공유 데이터나 자원이 정리되지 않은 지점에서 멈출 수 있다. 협력적 취소는 작업이 중단 요청을 관찰하고 자신이 맡은 정리를 수행하게 한다.[각주 1]
그 글이 지적한 기존 API의 어려움은 중단 방법이 객체마다 달랐다는 점이다. 작업 객체의 취소 메서드나 플래그에 의존하면 하나의 요청을 여러 작업에 전달하거나, 여러 작업 가운데 특정 실행만 구별하기가 어려워진다. 작업을 수행하는 객체와 실행 한 번의 수명을 분리할 필요가 생긴다.
이 기록은 .NET 설계자가 2009년에 설명한 문제와 설계 선택을 보여준다. 협력적 취소 전체의 발명 시점을 뜻하지는 않는다.
요청자와 관찰자를 별도 값으로 나누다 (2009)
.NET의 설계는 중단을 요청하는 CancellationTokenSource와 작업에 전달하는 CancellationToken을 나눈다. 호출자는 source로 중단 의사를 전달하고, 작업은 받은 token을 확인하거나 콜백을 등록한다. 여러 작업이 같은 요청을 관찰할 수도 있다.[각주 1]
이 분리는 취소 권한을 가진 쪽과 그 요청에 반응하는 쪽을 드러낸다. 현재 .NET 문서도 요청, 관찰, 작업별 정리를 협력적 취소의 절차로 설명한다. 요청을 보낸 시각과 작업이 실제로 끝난 시각 사이에는 간격이 있다.[각주 2]
결과의 모델로 표준화된 Promise (2015)
ECMAScript 2015는 Promise를 JavaScript 표준에 포함했다. Promise는 아직 정해지지 않은 결과와 이후의 성공·실패 반응을 표현하며, pending에서 fulfilled 또는 rejected로 정해진다.[각주 3]
하나의 결과를 여러 코드가 기다릴 수 있고 결과의 생산자가 통신을 하는지 계산을 하는지도 Promise만으로는 정해지지 않는다. 결과를 기다리는 인터페이스와 생산자의 실행 수명을 제어하는 인터페이스가 별도로 필요한 이유다.
타이머와 요청 중 먼저 정해진 Promise를 선택하면 기다림의 기한을 둘 수 있다. 남은 요청의 자원 사용이나 원격 서버의 처리를 끝내려면 그 작업이 지원하는 중단 수단도 연결해야 한다.
fetch에 취소를 연결한 웹 플랫폼의 논의 (2015~2017)
Jake Archibald의 「Abortable fetch」는 2015년에 시작된 논의가 2017년 브라우저 지원으로 이어진 과정을 기록한다. 글에 붙은 정정은 Edge 16이 먼저 지원했다는 점을 명시한다. 이 연혁의 대상은 웹 fetch의 취소 인터페이스다.[각주 4]
DOM의 AbortController와 AbortSignal도 요청자와 관찰자를 분리한다. controller가 중단을 요청하면 signal에 중단 상태와 이유가 남는다. API는 그 signal을 받아 자신의 실행을 어떻게 중단할지 정한다. 앞서 본 .NET과 비교할 수 있는 역할 분리이며, 두 인터페이스의 직접적인 계승 관계를 전제할 필요는 없다.[각주 5]
신호가 여러 API의 수명 계약으로 쓰이다
현재 DOM 표준은 진행 중인 활동의 중단을 위한 공통 신호를 정의하고, Fetch 표준은 이를 요청과 응답 본문에 연결한다. 응답 객체를 받은 뒤에도 본문 읽기가 진행될 수 있으므로, 응답 도착과 본문 처리 완료는 서로 다른 시점이다.[각주 5][각주 6]
협력적 취소의 흐름은 요청, 관찰, 정리, 완료로 이어진다. 긴 동기 계산은 신호를 관찰할 지점과 다른 코드가 실행될 기회가 필요하다. 신호 객체를 전달하는 것만으로 계산에 중단 지점이 생기지는 않는다.
외부 쓰기는 또 다른 수명을 가진다. 클라이언트가 통신을 중단했을 때 서버가 이미 커밋했다면, 그 변경의 처리 결과는 서버 조회나 별도의 취소 명령 같은 애플리케이션 계약으로 확인한다.
화면의 경쟁 상태에서 결과 적용 권한으로
검색 A 다음에 B를 시작해도 A가 늦게 끝날 수 있다. 완료 순서대로 화면에 적용하면 오래된 A가 최종 화면에 남는다. React 공식 문서의 Effect 정리 예제는 오래된 응답을 무시하는 방식으로 이 경쟁을 설명한다.[각주 7]
이 문제에서는 작업을 줄이는 중단 요청과 현재 화면을 바꿀 권한을 함께 관리한다. 요청 번호나 세대로 최신 요청을 구별하면 성공 결과뿐 아니라 오류 표시와 로딩 해제에도 같은 규칙을 적용할 수 있다. 부모 기능이 끝날 때 어느 하위 작업까지 끝내는지도 수명 계약에 포함된다.
중단 신호의 상태를 관찰하는 작은 예제
다음 코드는 controller와 signal의 역할을 먼저 확인한다. 통신이나 비동기 계산은 실행하지 않고, 중단 요청 전후의 상태만 읽는다.
+비동기 작업의 취소(cancellation)는 호출자가 더 이상 필요하지 않은 작업에 중단 의사를 전달하고, 그 작업이 종료되거나 결과가 더 사용되지 않도록 수명을 조정하는 일이다. 화면을 닫았는데도 이미지 다운로드가 이어지거나, 새 검색을 시작했는데 옛 검색 결과가 나중에 화면을 덮을 때 취소가 필요해진다.
중단하려는 대상에는 층이 여럿이다. 결과를 기다리던 코드를 끝내는 일, 계산이나 통신을 실제로 멈추는 일, 이미 끝난 작업의 결과를 적용하지 않는 일은 서로 다른 동작이고, 어느 층을 취소하는지는 API의 계약을 읽어 확인해야 한다.
작업을 시작한 뒤에는 호출자가 그 내부의 모든 실행을 직접 통제하기 어렵다. 네트워크 요청은 원격 서버로 넘어가고, 파일 읽기는 운영체제의 입출력과 연결되며, 계산은 이벤트 루프를 점유할 수 있다. 그래서 많은 API가 협력적 취소를 쓴다. 호출자는 취소 신호를 전달하고, 작업은 그 신호를 관찰해 스스로 정리한다.
취소는 작업 수명뿐 아니라 동시성 제어와도 연결된다. 새 검색의 시작과 옛 검색의 완료 순서에 따라 화면이 달라질 수 있으니, 작업을 멈추려는 시도와 화면을 바꿀 권한을 함께 살펴봐야 한다.
연혁
2009년 5월: Mike Liddell이 Microsoft .NET Blog의 「.NET 4 Cancellation Framework」에서 중단을 요청하는 CancellationTokenSource와 작업에 전달하는 CancellationToken을 나눈 .NET 4의 취소 설계를 소개했다. Parallel Extensions의 타입 전반에 이 방식을 적용했다고 밝힌 당시 설계 소개 글이다.[각주 1]
2010년 4월: CancellationTokenSource와 CancellationToken이 .NET Framework 4에 들어갔다.[각주 2][각주 3]
2015년 3월: WHATWG fetch 저장소에 「Aborting a fetch」 이슈 #27이 개설돼 취소 인터페이스를 논의했다.[각주 4][각주 5]
2015년 6월: ECMAScript 2015(ECMA-262 제6판)가 Promise를 JavaScript 표준에 넣었다.[각주 6]
2017년: 브라우저가 fetch 취소를 지원하기 시작했다. Jake Archibald의 「Abortable fetch」는 2015년 논의부터 이 과정을 기록하며, 글에 붙은 정정에 따르면 Edge 16이 먼저 지원했다.[각주 4]
강제 중단과 협력적 취소
Mike Liddell은 .NET 4 취소 프레임워크를 소개하는 글에서 실행 중인 코드를 강제로 끊는 접근과 중단을 요청하는 접근을 비교했다. 강제로 끊으면 공유 데이터나 자원이 정리되지 않은 지점에서 코드가 멈출 수 있다. 협력적 취소에서는 작업이 중단 요청을 관찰하고 자신이 맡은 정리를 직접 수행한다.[각주 1]
같은 글에 따르면 기존 API는 중단 방법이 객체마다 달랐다. 작업 객체의 취소 메서드나 플래그에 기대면 요청 하나를 여러 작업에 전달하기 어렵고, 여러 작업 가운데 특정 실행만 구별하기도 어렵다. 그래서 작업을 수행하는 객체와 실행 한 번의 수명을 분리할 필요가 생긴다.
.NET은 이 분리를 두 타입으로 나타낸다. 호출자는 CancellationTokenSource로 중단 의사를 전달하고, 작업은 전달받은 CancellationToken을 확인하거나 거기에 콜백을 등록한다. 같은 요청을 여러 작업이 관찰할 수도 있다.[각주 1] 이렇게 나누면 취소 권한을 가진 쪽과 그 요청에 반응하는 쪽이 코드에서 구별된다. 현재 .NET 문서도 요청, 관찰, 작업별 정리를 협력적 취소의 절차로 설명하며, 요청을 보낸 시각과 작업이 실제로 끝난 시각 사이에는 간격이 있다.[각주 7]
결과를 기다리는 일과 실행을 멈추는 일
Promise는 아직 정해지지 않은 결과와 그 뒤의 성공·실패 반응을 표현하며, pending에서 fulfilled 또는 rejected로 정해진다.[각주 6] 하나의 결과를 여러 코드가 기다릴 수 있고, 결과의 생산자가 통신을 하는지 계산을 하는지는 Promise만으로 정해지지 않는다. 그래서 결과를 기다리는 인터페이스와 생산자의 실행 수명을 제어하는 인터페이스가 따로 필요하다.
타이머와 요청 가운데 먼저 정해진 Promise를 고르면 기다림에 기한을 둘 수 있지만, 남은 요청의 자원 사용이나 원격 서버의 처리까지 끝내려면 그 작업이 지원하는 중단 수단을 함께 연결해야 한다.
취소 신호와 결과 적용 권한
웹 플랫폼에서는 DOM의 AbortController와 AbortSignal이 요청자와 관찰자를 나눈다. controller가 중단을 요청하면 signal에 중단 상태와 이유가 남고, signal을 받은 API가 자신의 실행을 어떻게 중단할지 정한다. 두 타입이 맡은 역할은 앞서 본 .NET의 요청자·관찰자 구분과 비교해 볼 수 있다.[각주 8]
현재 DOM 표준은 진행 중인 활동의 중단을 위한 공통 신호를 정의하고, Fetch 표준은 이 신호를 요청과 응답 본문에 연결한다. 응답 객체를 받은 뒤에도 본문 읽기는 계속될 수 있으므로, 응답 도착과 본문 처리 완료는 서로 다른 시점이다.[각주 8][각주 9]
협력적 취소는 요청, 관찰, 정리, 완료 순으로 진행된다. 긴 동기 계산은 신호 객체를 넘겨받는 것만으로는 멈추지 않는다. 계산 도중에 신호를 관찰할 지점이 있어야 하고, 다른 코드가 실행될 기회도 있어야 한다.
외부 쓰기는 수명이 따로 있다. 클라이언트가 통신을 중단했을 때 서버가 이미 커밋했다면, 그 변경이 어떻게 처리됐는지는 서버 조회나 별도의 취소 명령 같은 애플리케이션 계약으로 확인한다.
검색 A 다음에 B를 시작해도 A가 더 늦게 끝날 수 있고, 완료 순서대로 화면에 적용하면 오래된 A의 결과가 최종 화면에 남는다. React 공식 문서의 Effect 정리 예제는 오래된 응답을 무시하는 방식으로 이 경쟁을 설명한다.[각주 10]
여기서는 작업을 줄이는 중단 요청과 현재 화면을 바꿀 권한을 함께 관리한다. 요청 번호나 세대로 최신 요청을 구별해 두면 성공 결과는 물론 오류 표시와 로딩 해제에도 같은 규칙을 적용할 수 있다. 부모 기능이 끝날 때 하위 작업을 어디까지 끝낼지도 수명 계약에 들어간다.
취소 상태를 읽는 예제
다음 코드는 통신이나 비동기 계산 없이 중단 요청 전후의 signal 상태만 읽어서, controller와 signal의 역할을 확인한다.