← 기술 지식

기술 지식 / 웹·모바일 / iOS

ARC 참조 그래프와 비동기 완료 책임

강한 순환과 weak·unowned의 수명 전제를 나누고 화면·서비스·작업의 소유, 취소·완료 단일 전달을 설명합니다.

모바일 · 원리·구현·운영
기술 지식 트리

ARC는 참조 수가 0이 될 때 객체를 해제하는 규칙이고, 비동기 작업의 정확한 완료·취소를 조정하는 상태 머신은 별도 책임입니다. 화면, 서비스, 작업, 완료 클로저 사이의 강한 참조를 그린 뒤 작업의 생존과 UI 관찰의 생존을 나누면 weak를 무차별적으로 붙이는 오류를 피할 수 있습니다.

화면 종료와 잔존 강한 참조

예를 들어 화면이 서비스를 강하게 보관하고, 서비스가 진행 중인 작업을 보관한다고 합시다. 작업이 끝날 때 실행할 완료 클로저가 다시 화면을 강하게 캡처하면 화면 → 서비스 → 작업 → 클로저 → 화면 고리가 생깁니다. 화면을 화면 계층에서 제거해도 이 고리의 강한 참조가 0이 되지 않으므로, ARC는 이런 순환을 추적 GC처럼 대신 끊어 주지 않습니다.

누수가 의심되면 모든 self를 weak로 바꾸기 전에 누가 누구의 생존을 책임지는지 그립니다. 화면이 사라져도 끝내야 하는 다운로드·결제 결과가 있다면 작업의 소유자를 화면 밖에 두어야 합니다.

화면을 navigation stack에서 제거한 뒤에도 작업이 진행 중이면, 먼저 화면을 붙잡는 경로가 서비스의 필수 작업인지 완료 클로저의 UI 관찰인지 구분합니다. 화면이 사라져도 결제 결과를 보존해야 한다면 서비스가 작업을 소유해야 하고, 화면은 결과를 관찰할 때만 약하게 참조하는 구조가 자연스럽습니다.

참조 순환 해소와 필수 작업 소유

화면 관찰과 작업 소유를 분리합니다서비스: 필수 작업을 강하게 소유. 작업 객체: 실행 핸들 · 결과 · 정리. 완료 관찰 클로저: 화면은 weak 참조. 화면: 없으면 UI 반영 생략강한 소유완료까지 보관약한 관찰서비스필수 작업을 강하게 소유작업 객체실행 핸들 · 결과 · 정리완료 관찰 클로저화면은 weak 참조화면없으면 UI 반영 생략
화면 관찰과 작업 소유를 분리합니다 화살표는 참조 관계입니다. 서비스는 작업 완료를 책임지고, 완료 클로저의 화면 참조만 weak로 두어 화면 부재를 정상 처리합니다.
그림을 글로 읽기
  • 서비스: 필수 작업을 강하게 소유
  • 작업 객체: 실행 핸들 · 결과 · 정리
  • 완료 관찰 클로저: 화면은 weak 참조
  • 화면: 없으면 UI 반영 생략
  1. 서비스 → 작업 객체: 강한 소유
  2. 작업 객체 → 완료 관찰 클로저: 완료까지 보관
  3. 완료 관찰 클로저 → 화면: 약한 관찰

작업이 끝나면 서비스의 핸들과 작업의 클로저를 정리해야 합니다. weak 캡처가 있어도 서비스가 완료 작업을 영원히 모으면 메모리가 증가할 수 있습니다. 반대로 모든 참조를 weak로 만들면 필요한 작업이 너무 일찍 사라질 수 있습니다.

작업 시작, 화면 닫기, 작업 완료의 세 시점에서 강한 참조를 표로 기록합니다. 화면 닫기 후에도 service→task가 남아야 작업이 끝나고, 완료 후 task→closure와 service의 handle이 정리되어야 그래프가 끊깁니다. weak 캡처만 추가하고 작업 보관 컬렉션을 정리하지 않으면 누수는 남습니다.

weak와 unowned의 실패 방식 차이

참조 강한 수명 연장 대상이 먼저 사라지면
strong 예 소유자가 놓을 때까지 유지 가능
weak 아니오 자동 nil, 부재 처리 필요
일반적인 safe unowned 아니오 사용 시 수명 계약 위반으로 런타임 오류 가능

unowned는 자동으로 nil이 되는 weak의 짧은 표기가 아닙니다. 외부 큐·타이머·네트워크가 호출 시점을 정하는 비동기 클로저에서 대상이 먼저 사라지면, weak는 nil이 되어 부재 처리를 할 수 있지만 unowned는 살아 있다는 전제가 깨져 런타임 오류가 날 수 있습니다.

그래서 클로저가 사용하는 전체 기간 동안 대상이 살아 있다는 불변식을 설명할 수 있을 때만 unowned를 선택하고, 그렇지 않으면 weak 뒤에서 nil을 처리해야 합니다. unsafe unowned 같은 별도 기능은 더 강한 위험을 가지며 여기서는 다루지 않습니다.

완료 클로저에서 [weak self]로 캡처한 뒤 guard let self를 실행하면, 그 guard가 만든 강한 지역 참조는 해당 사용 범위 동안 화면을 유지합니다. await를 포함한 작업의 시작부터 끝까지 이 참조를 유지하면 화면을 닫아도 완료를 기다리는 동안 화면 수명이 늘어날 수 있습니다. 따라서 코드에 [weak self]가 있다는 표지만 보고 완료 시점까지 참조가 약하다고 단정하지 말고, 강한 참조가 만들어지고 풀리는 범위를 확인해야 합니다.

비동기 callback이 언제 호출될지 외부 큐가 결정하면 대상이 먼저 사라지는 정상 경로를 가정해야 합니다. weak는 nil 분기를 예측할 수 있지만 unowned는 그 전제가 깨질 때 런타임 오류가 될 수 있습니다. [weak self] 뒤에서 guard let self를 await 전 범위에 두는지에 따라 실제 수명도 달라지므로 사용 범위를 읽어야 합니다.

취소·완료 경쟁과 단일 결과 전달권

weak는 객체 생존을 다루지만 콜백이 한 번만 실행되는지는 보장하지 않습니다. 취소와 정상 완료가 동시에 오면 actor 또는 lock으로 pending 상태에서 한 전이만 사용자 결과 전달권을 얻도록 합니다.

finishUserResult(result):
    lock(state)
    if userResultState != pending:
        unlock(state)
        return
    userResultState = finished
    callback = completion
    completion = none
    unlock(state)
    dispatch_callback_on_required_executor(callback, result)

콜백은 잠금 밖에서 실행해 재진입과 교착을 줄입니다. 상태 검사와 변경 사이에 await를 두면 actor도 다른 작업이 끼어들 수 있으므로, 그 구간은 중단 없이 수행하거나 재검사해야 합니다. UI 변경은 해당 UI executor·MainActor 계약에 맞게 전달합니다.

취소와 정상 완료가 동시에 도착하는 실습에서는 두 경로 모두 pending을 읽어도 lock/actor 안에서 한 경로만 finished로 바뀌어야 합니다. callback은 잠금 밖에서 한 번 실행되고, 두 번째 경로는 조용히 반환되는 것이 예측 결과입니다. 이 상태 전이는 객체 수명과 별개로 검증합니다.

사용자 결과와 외부 효과의 분리 기록

취소가 먼저 사용자 응답을 확정했어도 서버에서 주문 생성이 성공했을 수 있습니다. 늦은 성공을 무조건 버리면 사용자는 실패로 알고 다시 주문할 수 있습니다. 작업 서비스가 실제 결과·불확정 상태를 기록하고 UI는 현재 요청 세대에 맞는 것만 표시하도록 나눕니다.

I/O가 계속 참조하는 버퍼·작업 핸들은 실제 종료까지 유지해야 합니다. 화면 참조가 nil이라는 사실은 버퍼를 즉시 반환해도 된다는 뜻이 아닙니다. 타이머 취소·callback 제거·작업 완료의 정리 책임을 한 소유자에게 모으는 편이 검증하기 쉽습니다.

취소 응답 뒤 서버 성공이 늦게 도착하면 UI의 현재 세대에는 표시하지 않더라도 서비스 원장에는 실제 결과를 기록해야 합니다. 그렇지 않으면 사용자의 재시도가 서버의 첫 주문과 중복될 수 있습니다. 화면 부재, callback 중복, 외부 결과 불확정을 서로 다른 상태로 관찰합니다.

메모리 그래프와 실행 순서의 통합 시험

화면 열기·닫기를 반복하며 객체 ID와 deinit, 서비스의 진행 작업 수를 기록합니다. 완료 전 화면 해제, 완료 직전 취소, 취소 직전 완료, 작업이 취소를 무시하는 경우를 나누어 검사합니다. 사용자 콜백은 한 번만 전달되고 실제 결과는 필요한 저장소에 남아야 합니다.

Swift·iOS 런타임의 실제 메모리 그래프와 MainActor 전환은 대상 환경에서 확인해야 합니다. 이 노트는 소유권·상태 전이 모형이며 단순히 deinit이 빨리 호출된 것을 작업의 정확성까지 증명한 것으로 보지 않습니다.

화면을 열고 닫는 반복 시험에서 객체 deinit, 진행 작업 수, callback 횟수, 외부 결과 기록을 함께 수집합니다. deinit이 빨라도 callback이 두 번 전달되면 정확성은 실패이고, callback이 한 번이어도 작업 핸들이 영원히 남으면 수명은 실패입니다. 두 축을 하나의 성공 판정으로 합치지 않습니다.

후속·관련 개념

이 개념의 확장

함께 읽을 문서

전체 기술 지식
선택 연습 · 관련 질문 3개

질문은 이해를 확인하는 보조 자료입니다. 학습 경로와 문서의 범위는 질문 수와 독립적입니다.