원격 Mac에서 iOS 백그라운드 파일 업로드 검증하기

원격 Mac에서 iOS 백그라운드 파일 업로드 검증하기

원격 개발 Mac에서 흔히 하는 오판이 있습니다. iOS 시뮬레이터에서 앱을 전면에 띄운 채 파일 업로드에 성공하면, 사용자가 다른 앱으로 전환한 뒤에도 작업이 유실되지 않을 것이라고 생각하는 것입니다. 백그라운드 업로드에는 디스크의 파일, 시스템이 이어받아 처리하는 전송 작업, 앱이 다시 실행될 때의 콜백이 모두 관여합니다. 요청이 전송됐는지만 확인해서는 부족합니다. 먼저 클라우드 Mac에서 실행하는 로컬 수신 서버로 1차 검증을 하고, 반드시 실기기에서 확인해야 할 항목을 따로 정리하겠습니다.

먼저 검증 범위 정하기

이번에 검증할 대상은 URLSessionConfiguration.background로 만든 파일 업로드 작업입니다. Data를 메모리에 보관한 채 네트워크 복구를 기다리는 방식이 아닙니다. 크기를 쉽게 구별할 수 있고 실제 인증 정보가 들어 있지 않은 샘플 파일을 준비해 바이트 수와 SHA-256을 기록하세요. 서버에서는 최소한 요청 메서드, 수신한 바이트 수, 응답 상태를 기록할 수 있어야 합니다. 클라이언트에서는 작업 식별자, 전송 오류, HTTP 상태 코드를 각각 기록합니다.

didCompleteWithError의 오류가 nil이라는 것은 전송 계층에서 오류를 보고하지 않았다는 뜻일 뿐입니다. 서버가 4xx나 5xx를 반환했을 수도 있으므로, 검증할 때는 HTTP 응답도 반드시 확인해야 합니다.

시뮬레이터는 이 기본 흐름을 확인하는 데 적합합니다. 하지만 특정 조건에서 시스템이 앱을 다시 실행하는지 실기기 대신 증명할 수는 없으며, 실제 모바일 네트워크 전환 테스트도 대체할 수 없습니다.

Mac에서 최소 수신 서버 실행하기

아래 스크립트를 receiver.py로 저장하고 시뮬레이터를 실행하는 Mac에서 실행하세요. 이 서버는 루프백 주소에서만 수신하고, 요청 한 건의 크기를 제한하며, 수신한 내용을 고정된 테스트 경로에 저장합니다. 인터넷에 공개하는 업로드 서비스로 사용해서는 안 됩니다.

from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

LIMIT = 10 * 1024 * 1024
OUTPUT = Path("/tmp/ios-upload-received.bin")

class Receiver(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        if self.path != "/upload" or not 0 < length <= LIMIT:
            self.send_error(400)
            return
        body = self.rfile.read(length)
        if len(body) != length:
            self.send_error(400)
            return
        OUTPUT.write_bytes(body)
        self.send_response(201)
        self.end_headers()

HTTPServer(("127.0.0.1", 8765), Receiver).serve_forever()

실행 명령은 python3 receiver.py입니다. 테스트 앱에서 http://127.0.0.1:8765/upload에 접근하려면, 테스트 빌드에 한해서 localhost 또는 사용 중인 루프백 호스트에 범위를 한정한 App Transport Security 예외를 설정하세요. 배포 빌드에서는 계속 HTTPS를 사용해야 합니다. 시뮬레이터에서 연결되지 않는다면 업로드 로직부터 수정하지 말고, 서버 프로세스가 실행 중인지와 포트가 일치하는지 확인한 다음 예외 설정을 살펴보세요.

디스크 파일로 백그라운드 작업 만들기

다음 예시는 작업을 생성하는 핵심 부분입니다. 호출하는 쪽에서 쓰기가 끝났고 현재 프로세스가 읽을 수 있는 파일 URL을 전달해야 합니다. endpoint는 테스트 설정에서 제공하고, 로컬 테스트 주소를 배포 코드에 하드코딩하지 마세요. 백그라운드 세션 식별자는 이 업로드 흐름에 전용으로 사용하고 일정하게 유지해야 합니다. 앱을 실행할 때마다 새 식별자를 만들지 마세요.

import Foundation

final class UploadCoordinator: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(
            withIdentifier: "com.example.upload-check"
        )
        config.isDiscretionary = false
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    func upload(file: URL, endpoint: URL) {
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        session.uploadTask(with: request, fromFile: file).resume()
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        let status = (task.response as? HTTPURLResponse)?.statusCode
        let accepted = error == nil && status == 201
        recordResult(taskID: task.taskIdentifier, accepted: accepted)
    }

    func urlSessionDidFinishEvents(
        forBackgroundURLSession session: URLSession
    ) {
        finishPendingBackgroundEvents()
    }

    private func recordResult(taskID: Int, accepted: Bool) {
        // 결과를 앱의 영속 상태 기록기에 전달
    }

    private func finishPendingBackgroundEvents() {
        // 앱의 상태 업데이트가 끝난 뒤 저장해 둔 시스템 완료 핸들러 호출
    }
}

예시의 두 private 메서드는 앱에 통합할 지점을 표시할 뿐, 업로드 완료 처리가 구현됐다는 뜻은 아닙니다. 실제로 연동할 때는 영속적으로 기록한 결과에서 작업과 업무용 파일의 대응 관계를 알 수 있어야 합니다. 앱 델리게이트가 백그라운드 세션 이벤트를 받으면 같은 식별자로 세션을 다시 만들고 델리게이트를 연결한 뒤, 시스템이 제공한 완료 핸들러를 식별자별로 저장하세요. 이벤트 처리와 화면 상태 업데이트가 끝난 다음 그 핸들러를 호출합니다. 성공 콜백 하나만 보고 아직 검증하지 않은 업무용 파일을 바로 삭제해서는 안 됩니다.

단계별로 검증하고 강제 종료를 백그라운드 전환으로 간주하지 않기

먼저 앱을 전면에 둔 상태에서 샘플 파일을 한 번 제출합니다. 수신 서버가 201을 반환하면 /tmp/ios-upload-received.bin과 원본 파일의 SHA-256을 비교하세요. 두 값이 같아야 이번에 수신한 내용이 예상한 전체 파일이라고 판단할 수 있습니다. 다음에는 다른 샘플을 사용해 작업을 제출한 뒤, 시뮬레이터 화면에서 홈 화면으로 돌아가 콜백을 기다리고 파일을 다시 대조합니다. 명령으로 프로세스를 강제 종료해 일반적인 ‘백그라운드 전환’을 흉내 내지 마세요. 사용자가 앱을 직접 강제 종료하는 것과 시스템이 작업을 이어받은 뒤 앱이 복구되는 것은 다른 상황입니다.

확인 지점 기록할 증거
작업 생성 세션 식별자, 작업 식별자, 원본 파일 경로와 바이트 수
서버 수신 HTTP 상태, 실제 수신 바이트 수, 파일 SHA-256
클라이언트 완료 전송 오류와 HTTP 상태를 각각 기록
앱 복구 세션 재생성, 콜백 도착, 완료 핸들러 호출 순서

실기기 검증에서는 화면 잠금, 네트워크 중단 후 복구, 네트워크 유형 전환, 시스템이 앱을 종료한 뒤 다시 실행하는 상황도 각각 확인해야 합니다. 사용자가 앱을 직접 강제 종료하는 경우는 별도 테스트로 두고, 그 결과를 일반적인 백그라운드 동작으로 간주하지 마세요. 원격 Mac에서 가까이에 있는 기기에 연결할 수 없다면 시뮬레이터에서 확보한 증거와 남은 테스트 목록을 실기기를 가진 동료에게 전달하세요. 보고서에 ‘실기기 검증 완료’라고 적어서는 안 됩니다.

자주 발생하는 실패와 마무리 점검

서버에 요청이 도착하지 않았다면 먼저 파일 URL이 실제로 존재하는 일반 파일을 가리키는지 확인하고, 이어서 테스트 주소, 포트, 전송 보안 설정을 점검하세요. 파일은 도착했는데 클라이언트가 실패로 표시한다면 응답 상태를 확인하세요. 이 예시는 201만 성공으로 인정하므로, ‘전송 오류가 없음’을 ‘서버가 수신함’과 동일하게 취급해서는 안 됩니다. 앱 복구 후 콜백이 오지 않는다면 기존 식별자로 백그라운드 세션을 다시 만들었는지, 델리게이트 객체가 계속 유지되는지, 완료 핸들러를 너무 일찍 호출하지 않았는지 확인하세요.

마지막으로 테스트 수신 서버를 종료하고 /tmp/ios-upload-received.bin을 삭제한 뒤, 테스트 전용 원본 파일과 네트워크 예외 설정을 정리하세요. 운영 흐름에서는 실패 시 재시도 횟수의 상한과 중복 제출에 대한 서버 처리 방식도 정의하고, 결과가 영속적으로 저장됐는지 확인한 후 파일을 정리해야 합니다. 다음 엔지니어에게 넘겨야 할 것은 ‘백그라운드 업로드가 된다’는 한마디가 아니라, 다시 실행할 수 있는 절차와 대조 가능한 증거, 그리고 아직 끝내지 못한 실기기 테스트 항목입니다.

자주 묻는 질문

백그라운드 URLSession에서 메모리의 Data를 바로 업로드할 수 있나요?

이 절차에서는 파일 기반 업로드를 사용합니다. 데이터를 먼저 디스크에 기록한 뒤 uploadTask(with:fromFile:)로 작업을 만드세요.

시뮬레이터에서 성공하면 앱 재실행 후 복구도 보장되나요?

아닙니다. 시뮬레이터는 요청, 응답, 기본 콜백 확인에 쓰고 시스템의 앱 재실행, 네트워크 전환, 강제 종료 후 동작은 실기기에서 별도로 검증해야 합니다.

전용 물리 서버

다음 원격 개발을 위한 Mac 준비하기

용도에 맞는 모델과 리전을 선택하고 화면 공유, VNC 또는 SSH로 연결하세요. 가격과 실제 이용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.

원격 Mac 구성하기