사용자가 잠시 전송을 멈췄는데 자동화가 시간이 지났다며 취소하면 화면의 제어권과 실행기의 판단이 충돌한다. 북극성에서는 일시정지한 시간을 전송 제한 시간에서 빼되, 작업 자체를 취소한 신호는 계속 읽도록 만들었다. 기다림을 한 숫자로만 다루지 않게 된 과정이다.
멈춘 이유를 먼저 읽는다
다운로드가 진행되지 않는다고 모두 같은 상태는 아니다. 네트워크가 늦을 수도 있고, 네이티브 item이 일시정지 상태일 수도 있고, 사용자가 작업 전체를 끝내라고 했을 수도 있다. 첫 번째에는 제한 시간이 필요하고 두 번째에는 사람의 선택을 존중할 시간이 필요하며 세 번째에는 정리가 필요하다.
downloadFrom은 remaining과 lastTick을 두고 주기적으로 지난 시간을 계산한다. 아직 item을 잡지 못했거나 전송이 끝나 해시를 읽는 중이거나 item이 멈춰 있지 않을 때 예산을 차감한다. 네이티브 일시정지만 예외다. 오류가 났다는 이유로 모든 대기를 무제한 연장하거나 전체 작업 취소까지 무시하는 구현은 아니었다.
일시정지는 타이머를 없애는 기능이 아니다
검사 주기는 50밀리초이며 실제 시각의 차이로 예산을 갱신한다. 멈춤 전후를 관찰하는 샘플링 방식이므로 사용자의 클릭 시각까지 완벽하게 분해한 시간 계측기라고 표현하지 않는다. 관찰된 일시정지 구간은 전송 실패를 판단하는 대기 시간에서 제외한다.
remaining이 끝나면 진행 중 item을 취소하고 DOWNLOAD_TIMEOUT을 돌려준다. 전송이 완료된 뒤 해시 읽기에도 시간이 걸릴 수 있어 그 구간은 예산을 쓴다. 제한 시간을 데이터 전송 버튼을 누른 순간부터만 의미 있다고 보면 실제 결과를 읽는 비용이 빠진다. 나는 결과를 받을 준비까지를 기다리는 호출의 책임으로 봤다.
작업 취소는 멈춘 전송에서도 계속 확인한다
주기 검사의 앞부분에서 check를 실행한다. 사용자가 item을 멈춰 둔 상태라도 작업 취소가 들어오면 현재 전송을 취소하고 기다리는 호출을 거절한다. 취소와 일시정지를 같은 버튼의 다른 이름처럼 다루지 않은 이유다. 사람이 잠시 멈춘다는 뜻과 더 이상 하지 말라는 뜻은 다르다.
전송 취소 함수는 item이 있고 아직 transferDone이 아닐 때만 네이티브 cancel을 시도한다. 이미 종료된 객체가 해제된 경우도 고려한다. 마지막 정리에서 타이머와 세션 리스너를 제거해 다음 작업이 지난 대기의 영향을 받지 않게 한다. 이 함수가 작업의 모든 다른 후속 동작까지 자동 취소한다는 의미는 아니다.
늦은 버튼이 완료를 취소로 바꾸면 안 된다
ActiveDownloads.perform은 버튼을 누를 때 현재 프로필과 살아 있는 item을 확인하고 상태를 다시 읽는다. 화면에 남은 일시정지 버튼이 오래됐으면 현재 완료 상태를 우선한다. update가 terminal을 발견하면 항목을 해제하고, 더 이상 수행 가능한 제어가 없으면 요청을 거절한다.
cancel 안에서 동기적으로 done이 발생해 객체가 사라지는 경우도 따로 다룬다. 취소 호출 뒤에도 같은 active 객체가 남아 있을 때만 다시 상태를 읽는다. 원본 시험에는 취소 중 실제 완료가 먼저 확정되고 이후 네이티브 객체 접근이 실패하는 대역을 넣었다. 완료된 파일을 옛 제어 요청으로 취소라고 덮지 않기 위한 경쟁 조건이다.
짧은 예산보다 오래 멈춰도 실패하지 않는지 본다
원본 일시정지 시험은 제한 시간보다 오래 item을 멈춰 둔 뒤 취소 횟수가 늘지 않았는지 보고, 재개한 완료 파일의 크기를 확인한다. 다른 시험은 멈춘 item에서도 작업 취소가 실제 cancel을 호출하는지 본다. 두 조건 중 하나만 통과하면 사용자가 기대하는 제어권을 설명하기 어렵다.
이번 집필은 해당 코드와 시험의 검토이며 원래 앱이나 네트워크 전송을 새로 실행한 검수가 아니다. 함께 공개한 영수증 모듈은 바이트 비교만 다루며 타이머·일시정지를 구현하지 않는다. 어떤 공유 코드에 어떤 기능이 있는지도 구분했다. 이 글에서 가져갈 기준은 의도적인 대기와 실패를 나누고, 취소 신호는 그 구분 밖에서도 계속 존중하는 것이다.