Swift ARC의 strong·weak·unowned: 수명 관계로 참조 고르기

반응형

Swift의 class instance는 Automatic Reference Counting, 즉 ARC가 수명을 관리한다. ARC는 strong reference 수를 추적하다가 더는 강하게 참조되지 않는 instance를 해제한다.

여기까지만 보면 직접 memory를 해제할 일이 없어 보인다. 하지만 두 instance가 서로를 strong으로 잡거나, instance가 자신을 잡는 closure를 보관하면 reference count가 0이 되지 않는 cycle이 생길 수 있다.

weakunowned는 이 cycle을 끊는 도구다. 둘 중 무엇을 고를지는 문법 취향이 아니라 참조 대상이 먼저 사라질 수 있는가로 결정해야 한다.

strong은 기본 소유 관계다

별도 keyword 없이 class instance를 property에 담으면 strong reference다.

final class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
    }
}

final class Apartment {
    let unit: String
    var tenant: Person?

    init(unit: String) {
        self.unit = unit
    }
}

다음처럼 서로를 연결하면 PersonApartment가 상대를 계속 소유한다.

var person: Person? = Person(name: "Ben")
var apartment: Apartment? = Apartment(unit: "101")

person?.apartment = apartment
apartment?.tenant = person

person = nil
apartment = nil

외부 변수 두 개를 nil로 바꿔도 두 instance 사이의 strong reference가 남는다. 이것이 strong reference cycle이다.

weak: 대상이 먼저 사라질 수 있을 때

임차인은 이사할 수 있고 apartment보다 먼저 사라질 수 있다. 이런 관계는 weak가 잘 맞는다.

final class Apartment {
    let unit: String
    weak var tenant: Person?

    init(unit: String) {
        self.unit = unit
    }
}

weak reference의 성질은 세 가지다.

  • 참조 대상의 strong reference count를 올리지 않는다.
  • 대상이 해제되면 ARC가 자동으로 nil로 바꾼다.
  • 실행 중 nil이 될 수 있으므로 일반적으로 optional var로 선언한다.

delegate가 owner보다 먼저 없어질 수 있는 UI code, 비동기 callback에서 호출자가 이미 화면을 떠날 수 있는 경우처럼 수명이 독립적이면 weak가 안전한 기본 선택이다.

protocol DownloadDelegate: AnyObject {
    func downloadDidFinish()
}

final class Downloader {
    weak var delegate: DownloadDelegate?
}

AnyObject 제약은 delegate가 class reference여야 weak로 보관할 수 있다는 뜻이다.

unowned: 대상이 반드시 더 오래 살아 있을 때

unowned도 strong count를 올리지 않는다. 차이는 대상이 해제돼도 자동으로 nil이 되지 않는다는 점이다. 이미 해제된 instance에 접근하면 runtime error가 발생한다.

final class Customer {
    let name: String
    var card: CreditCard?

    init(name: String) {
        self.name = name
    }
}

final class CreditCard {
    let number: UInt64
    unowned let customer: Customer

    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

이 model은 “card는 언제나 customer에 속하고 customer보다 오래 살지 않는다”는 invariant가 지켜질 때만 안전하다. 다른 곳에서 CreditCard를 strong으로 오래 보관한 뒤 Customer가 먼저 해제되면 card.customer 접근은 crash로 이어진다.

따라서 unowned는 optional을 피하기 위한 편의 기능이 아니다. 다음 질문에 확실히 답할 수 있을 때 쓴다.

이 참조를 읽는 모든 순간에 대상이 살아 있다는 것을 구조적으로 보장할 수 있는가?

보장하기 어렵다면 weak가 낫다.

weak와 unowned 비교

기준 weak unowned
strong count 증가 하지 않음 하지 않음
대상 해제 뒤 값 자동으로 nil 유효하지 않은 참조가 됨
일반적인 type optional var non-optional로 자주 사용
잘못 접근했을 때 optional 처리 경로 runtime error
적합한 관계 대상이 먼저 사라질 수 있음 대상이 같거나 더 긴 수명임을 보장

Swift는 optional unowned reference도 지원한다. 하지만 weak처럼 ARC가 자동으로 nil로 바꿔 주는 것은 아니다. 값이 가리키는 instance가 살아 있다는 invariant를 개발자가 계속 지켜야 하므로, 단지 optional이라는 이유로 weak와 같다고 보면 안 된다.

unowned(unsafe)는 해제된 대상 접근에 대한 runtime safety check까지 끈다. dangling memory를 읽을 수 있어 특별한 low-level 이유와 별도 검증이 없다면 사용하지 않는 편이 맞다.

closure가 self를 강하게 잡는 경우

property에 저장된 escaping closure가 self를 사용하면 cycle이 생길 수 있다.

final class ImageLoader {
    var onComplete: (() -> Void)?

    func start() {
        onComplete = { [weak self] in
            guard let self else { return }
            self.finish()
        }
    }

    private func finish() {
        print("finished")
    }
}

ImageLoaderonComplete를 strong으로 소유하고, closure가 다시 self를 strong으로 capture하면 cycle이 된다. [weak self] capture list는 closure 안의 self를 optional weak reference로 바꾼다.

[unowned self]도 cycle을 끊지만 closure가 instance보다 오래 남는 순간 crash할 수 있다. network callback, timer, task, delegate처럼 cancellation과 실행 시점이 외부 상태에 좌우되면 weak를 우선 검토하는 편이 안전하다.

반대로 closure와 instance가 반드시 함께 해제된다는 lifetime invariant가 명확하면 unowned를 선택할 수 있다. 중요한 것은 closure가 “곧 실행될 것 같다”는 기대가 아니라 code 구조가 수명을 보장하는지다.

선택 순서

reference cycle을 발견했을 때는 모든 연결을 weak로 바꾸기보다 ownership을 먼저 그려 보는 편이 낫다.

  1. 누가 누구를 소유해야 하는가?
  2. 두 객체 중 어느 쪽이 먼저 사라질 수 있는가?
  3. 대상이 사라졌을 때 nil을 정상 상태로 처리할 수 있는가?
  4. 비동기 closure가 cancel되거나 늦게 실행될 수 있는가?
  5. unowned 대상의 수명을 test로 깨뜨려 보았는가?

수명이 독립적이면 weak, 대상이 반드시 더 오래 산다는 invariant가 있으면 unowned, 실제 ownership이면 strong이다.

Swift closure와 callback 정리에서 capture 동작을 먼저 확인하고, SwiftUI에서 class와 struct를 고르는 기준까지 이어 보면 reference semantics가 필요한 위치도 함께 정리할 수 있다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글