Swift Language Guide: ARC (Automatic Reference Counting)

Automatic Reference Counting

서문

  • Swift는 APP의 메모리 usage를 관리하고 추적하기 위해 ARC를 사용한다
  • 그래서 대부분의 경우, 메모리는 알아서 관리되므로 고려하지 않아도 된다
  • ARC는 Class 인스턴스가 더이상 필요없다고 판단될때 자동으로 메모리를 해제한다
  • 하지만 일부 케이스에선, ARC가 코드들 간의 관계에 대한 정보를 요구하기도 한다
  • 이 예외 케이스에 대해 구체적으로 알아보고
  • ARC가 어떻게 메모리를 자동으로 관리하는지 살펴보자
  • 참고로, Reference Count 라는건 참조타입인 Class의 인스턴스에만 해당된다

동작 이해

  • Class의 인스턴스를 생성하면 ARC가 메모리를 할당하게 된다

  • 이 메모리에는 인스턴스의 타입이나 저장 프로퍼티의 값들이 저장된다

  • 이후에 인스턴스가 더이상 필요없다고 판단되면, 메모리 자원 활용을 위해 해당 인스턴스에 잡혀있던 메모리를 해제한다

  • usage가 남아있는 인스턴스의 메모리를 해제해버리면 APP이 Crash나므로 인스턴스가 필요없는게 맞는지 추적해야 한다

  • 각 인스턴스가 현재 얼마나 많은 프로퍼티 + 상.변수에게 참조되고 있는지를 Count 하는 방식으로 추적한다

  • 해당 인스턴스에 대해 Active Reference가 1개라도 있다면 해제하지 않는다

  • 구체적으론, 인스턴스를 어떤 프로퍼티+상.변수로 참조시킬때마다 강한 참조라는게 만들어진다

  • "강한"이란 표현을 쓴 이유는 약한참조도 존재한다는 뜻이며 (추후설명) 강한 참조가 남아있다면 메모리는 해제되지 않는다

동작 예시

  • 인스턴스를 1개 만들어 3개의 변수가 참조하도록 만들었다
  • 3개의 강한 참조가 존재하는 것이며 3개 모두 nil로 참조를 해제한 시점에서야 비로소 인스턴스가 해제되었다
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil

reference3 = nil
// Prints "John Appleseed is being deinitialized"

강한 참조 순환 @Class 인스턴스 간

  • 강한 참조가 절대 사라질 수 없는 코드가 존재할 수 있다
  • 두 Class 인스턴스가 서로를 강한 참조하는 경우이다. 이를 강한 참조 순환이라고 한다
  • 이를 해결하는 방법으로, 일부 Class간 관계를 weak 참조 혹은 unowned 참조로 정의할 수 있다

  • 약한 참조를 배우기 전에 강한 참조 순환이 어떻게 발생하는지 살펴보자

  • 예제

    #1

    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }
    
    var john: Person?
    var unit4A: Apartment?
    
    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")
    
    john!.apartment = unit4A
    unit4A!.tenant = john
    //참고로, 내부 프로퍼티에 접근하려면 Unwrapping 해야한다

    • 두 Class 인스턴스가 서로를 참조하고 있다

      #2

      john = nil
      unit4A = nil

    • john과 unit4A의 참조를 끊어도 인스턴스간 연결은 끊어지지 않아 메모리가 해제되지 않고 소멸자는 호출되지 않는다

강한 참조 순환 해결방법 @Class 인스턴스 간

  1. 개념

    • 2가지 방법을 제공한다. weakunowned

    • 두 keyword는 인스턴스 간 참조하여도 강하게 hold하지 않고 강한 참조 Cycle을 형성하지 않는다

    • weak : 참조하려는 인스턴스가 먼저 해제될 때

      Ex) 위의 코드에서 Apartment를 살펴보면 자기 자신보다 거주자에 해당하는 Person이 먼저 해제될 것 같으니 weak를 사용할 수 있다

    • unowned : 참조하려는 인스턴스가 더 나중에 혹은 동시에 해제될 때

  2. weak

  • weak로 참조된 것은 연결을 끊지 않아도 ARC에 의해 해제될 수 있다
  • 반드시 Optional 타입의 변수(variable)로 선언해야 한다. ARC가 nil로 값을 변경하여 해제하기 때문
  • 그리고 사용할 때는 nil 체크를 동반하면 된다

💡 참고로, ARC에 의한 값변경(→ nil)은 Property Observer를 호출하진 않는다

  • 예제 #1
    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        weak var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }
    
    var john: Person?
    var unit4A: Apartment?
    
    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")
    
    john!.apartment = unit4A
    unit4A!.tenant = john
    • Person 인스턴스를 살펴보자.

    • 이전 예시와는 달리 강한 참조는 john이 유일하다

      #2

      john = nil
      // Prints "John Appleseed is being deinitialized"

    • 유일한 강한 참조인 john을 해제하면 Person 인스턴스를 강한 참조하는 것이 존재하지 않게 되고 Person 인스턴스도 ARC에 의해 해제된다

    • 참조하던 인스턴스가 해제되면 자동으로 weak 참조는 nil로 설정된다

      #3

      unit4A = nil
      // Prints "Apartment 4A is being deinitialized"

  1. unowned
  • weak와 마찬가지로 ARC의 자동 메모리 해제를 막지 않는다

  • 하지만 unowned는 weak와 반대로, 참조 대상이 동시에 or 더 늦게 해제될 것 같을 때 사용된다 (=값이 있음을 확신할 수 있을 때)

  • 또한, weak와는 달리 참조 대상이 해제되어도 자동으로 nil이 되지 않는다

  • 그러므로 unowned는 항상 값이 있다고 가정되므로 Optional 타입일 필요가 없고 상수로도 사용가능하다

  • 예제 - 두 인스턴스가 동시에 해제되는

    #1

    class Customer {
        let name: String
        var card: CreditCard?
        init(name: String) {
            self.name = name
        }
        deinit { print("\(name) is being deinitialized") }
    }
    
    class CreditCard {
        let number: UInt64
        unowned let customer: Customer
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
        deinit { print("Card #\(number) is being deinitialized") }
    }
    • Customer는 Card가 없을 수 있지만 / Card는 customer가 반드시 있다

      = 적어도 Customer가 혼자 먼저 해제되는 경우는 없다

      = 동시에 해제되거나 Card가 먼저 해제될 것이다

      = Customer를 unowned로 선언

    • 이를 위해, CreditCard의 생성자에 customer를 반드시 받도록 정의하였다

      #2

      var john: Customer?
      
      john = Customer(name: "John Appleseed")
      john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

      #3

      john = nil
      // Prints "John Appleseed is being deinitialized"
      // Prints "Card #1234567890123456 is being deinitialized"

    • john의 강한 참조를 끊으면 Customer 인스턴스에 대한 어떤 강한 참조도 없으므로 Customer 인스턴스가 해제된다

    • 동시에, CreditCard 인스턴스에 대한 강한 참조도 사라져 해제된다

  1. Optional unowned
  • unowned를 Optional 타입으로 선언하면 사실상 weak와 같은 문맥에서 사용된다

  • Optional 타입이므로 nil check가 동반된다

  • 예제

    #1

    class Department {
        var name: String
        var courses: [Course]
        init(name: String) {
            self.name = name
            self.courses = []
        }
    }
    
    class Course {
        var name: String
        unowned var department: Department
        unowned var nextCourse: Course?
        init(name: String, in department: Department) {
            self.name = name
            self.department = department
            self.nextCourse = nil
        }
    }
    
    let department = Department(name: "Horticulture")
    
    let intro = Course(name: "Survey of Plants", in: department)
    let intermediate = Course(name: "Growing Common Herbs", in: department)
    let advanced = Course(name: "Caring for Tropical Plants", in: department)
    
    intro.nextCourse = intermediate
    intermediate.nextCourse = advanced
    department.courses = [intro, intermediate, advanced]

    • Course는 없을 수 있어도 Department는 반드시 존재해야 한다
    • 연계 Course가 아닐 수 있으므로 nextCourse는 없을 수 있어 Optional로 선언
    • Department → Course로의 강한 참조만 해제하면 인스턴스가 해제될 수 있다
  1. 암시적 추출이 필요한 경우가 있다

아래 코드와 같이 생성자 내에서 다른 Class의 생성자를 호출하는 경우이다

capitalCity가 만약 Optional이 아니라면?

City 생성자를 호출할 때 self를 전달하는데 저번에 배웠듯이 fully initialized되기 전에는 self를 사용할 수 없어서 호출이 안된다

Point
Optional로 정의하면 자동으로 nil 초기화되므로 name만 값을 설정해주면 fully initialized 되어 self를 사용할 수 있게 된다

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

이렇게 서로 참조하는 프로퍼티를 가진 관계의 Class가 생성자 내에서 인스턴스를 생성하려는 경우

self를 사용하기 위해 암시적 추출 Optional을 활용할 수 있다

Strong Reference Cycles for Closures

이전까지는 여러 Class가 서로의 인스턴스를 프로퍼티로 가지는 경우였다

순환참조가 발생하는 또 다른 경우를 살펴보자

바로, self를 참조하는 Closure를 프로퍼티로 가졌을 때이다

이 상황을 Closure가 self를 capture했다고 표현한다

코드를 먼저 보자.

class HTMLElement {

    let name: String
    let text: String?
		//asHTML 변수는 self를 참조하는 Closure 프로퍼티이다
		//참고로 lazy 선언이 없으면 Two-Phase rule에 위배되어 
		//   self를 참조하는 default를 설정할 수 없다
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

Point
Closure가 순환참조를 유발할 수 있는 이유는 Closure 역시 참조타입이기 때문이다

본체가 어딘가에 따로 있고 asHTML이 그것을 강한참조하고 있는 것이다

Closure 역시 내부에서 self를 강한참조하고 있으므로 순환참조가 발생한다

❗ 주의
Closure가 self를 여러 번 참조하고 있지만, 1개의 강한참조만 존재한다

Resolving Strong Reference Cycles for Closures

인스턴스와 Closure간 순환참조는 어떻게 해결할까? weak나 unowned??

이 경우엔 다른 해결책이 제시된다.

그것은 바로.. **Closure Capture List!**

이것 또한 참조를 약하게 만드는 방식 중 하나이다

❗ 주의
Swift는 Closure 내에서 member를 참조할때 self를 붙일 것을 요구한다
Closure가 실수로 self를 capture할 수 있음을 인지하는데 도움을 준다

Defining a Capture List

참조를 약하게 만들고싶은 것들을 weak나 unowned 키워드와 쌍으로 명시해준다

예시를 살펴보자

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate]
    (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

Weak and Unowned References

우선 unowned를 사용해야 하는 경우는

Instance가 항상 존재한다고 생각할 때이다

이 경우 Instance와 Closure는 운명 공동체이므로 동시에 해제될 것이다

반대로 weak를 사용해야 하는 경우는

참조하는 Instance가 (미래의 어느 시점?) nil이 될 수 있음을 알고 있다

그래서 weak로 선언하면 Optional 타입으로 접근해야 한다

또한, Instance가 해제되면 자동으로 해당 참조는 nil이 된다

Point
Capture하려는게 사용시점에 nil이 될 수 없다면 unowned를 쓰고
nil이 될 수 있다면 weak를 써라

위 예제에서 참조순환을 풀어보자

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

여기선 왜 unowned를 선택했을까?

아직 명확한 정답이 떠오르진 않지만 생각을 말해보면

Closure가 self의 프로퍼티인데 자기자신(self)을 해제하고 사용되는 경우가 존재할까?

좋은 웹페이지 즐겨찾기