ARC와 Reference Cycle 그리고 weak, unowned 로 해결하기

yaja·2022년 1월 25일
1

Swift 개념 다지기

목록 보기
1/1
post-thumbnail

ARC는 Auto Reference Counting의 약자로, iOS에서 메모리를 관리해주는 역할을 한다.

객체가 몇 번 참조 되었는 지를 Count 하고, 참조 카운트가 0이 되면 객체를 메모리에서 해제시킨다.

매번 전달할 때마다 값을 복사해 전달하는 값 타입(value type)과 달리 참조 타입(reference type)은 하나의 인스턴스에 참조를 통해 여러 곳에서 접근할 수 있기 때문에, 언제 메모리에서 해제되는 지는 중요한 문제다.

Strong Reference

기본적으로 클래스를 인스턴스화해서 객체를 생성했을 때, 객체는 인스턴스에게 강한 참조(Strong Reference)를 한다. 여기서 객체의 Reference Count는 1이 된다.

이 때 객체에 nil 값을 할당하면 강한 참조는 사라지게 되고, Reference Count는 0이 된다. 그로 인해 해당 객체는 메모리에서 해제된다.

참조의 default는 강한 참조이기 때문에 별도의 식별자를 명시하지 않을 경우 Strong Reference를 하게 된다.

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

위와 같은 Person 클래스가 있고, 다음과 같이 Person 타입의 변수를 선언한다.

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

위의 변수들은 Optional 이기 때문에 초기값으로 nil을 가진다.

reference1에 하나의 인스턴스를 할당한 뒤, 남은 변수들이 reference1을 참조하도록 하려면 다음과 같이 코드를 작성한다.

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

이 경우 name 변수의 값이 John Applessed인 Person 인스턴스의 RC는 3이 되고, 각각 변수들은 Person 인스턴스에 대해 강한 참조를 하고 있다.

Person 인스턴스를 메모리에서 해제하기 위해서는 다음과 같이 각각 변수에 명시적으로 nil을 할당해주어야 한다.

reference1 = nil
reference2 = nil
reference3 = nil
// print("\(name) is being deinitialized")

Strong Reference Cycles Between Class Instances

그러나 명시적으로 nil을 할당하더라도, 메모리에서 영원히 해제되지 않는 케이스도 발생한다.

이를 강한 순환 참조(Strong Reference Cycle) 이라고 하는데, 어떤 경우에 강한 순환 참조가 발생할까?

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") }
}

위와 같이 아파트, 이름 정보를 갖고 있는 Person 클래스와

단지 정보, 해당 아파트에 살고 있는 세입자 정보를 가지고 있는 Apartment 클래스가 있다고 하자.

var yeji = Person?
var unit4A: Apartment?

yeji = Person(name: "yeji yun") // Person RC = 1
unit4A = Apartment(unit: "4A") // Apartment RC = 1

Person 타입의 변수와 Apartment 타입의 변수를 위와 같이 선언하고, 각각의 변수에 인스턴스를 할당해준다.

위와 같은 코드를 그림으로 나타내면 다음과 같다.

strong reference는 각 변수에 명시적으로 nil을 할당함으로서 Reference Count를 줄일 수 있음.

이제 각각 클래스의 Optional Type인 apartment와 tenant에 값을 할당하려한다.

unit4A의 세입자는 yeji이다.

그렇다면 다음과 같이 값을 할당해야한다.

yeji!.apartment = unit4A // Apartment RC = 2
unit4A!.tenant = yeji // Person RC = 2 

위의 그림은 아래와 같이 바뀐다.

이 상황에서 더 이상 yeji란 사람이 존재하지 않게 됐고, 아파트엔 불이 나서 없어졌다.

그렇다면 각각의 변수에 nil을 할당해서 yeji와 unit4A를 없애야한다.

yeji = nil
unit4A = nil

그러나 yeji와 unit4A는 여전히 존재한다.

왜냐하면 각각 변수의 Reference Count는 여전히 1이기 때문에, ARC는 해당 변수들을 메모리에서 해제하지 않는 것이다.

yeji의 apartment 변수는 Apartment Instance를, unit4A의 tenant 변수는 yeji를 참조하고 있기 때문에 각각의 RC가 여전히 1인 것이다.

Resolving Strong Reference Cycles Between Class Instances

이러한 문제는 weak, unowned 참조를 사용함으로써 해결할 수 있다.

weak, unowned 참조는 ARC에서 Reference Count를 증가시키지 않고 인스턴스를 참조하기 때문이다.

weak 참조는 참조하고 있는 인스턴스가 먼저 메모리에서 해제될 때 사용하고,

unowned 참조는 참조하고 있는 인스턴스가 같은 시점 혹은 클래스보다 이후에 해제될 때 사용한다.

Apartment 안에 세입자는 존재하지 않을 수 있으므로(= nil 값이 될 수 있으므로), 위의 예제에서는 tenant를 weak로 선언해주는 것이 적절하다.

Weak Reference

변수 앞에 weak 키워드를 붙여줌으로써 사용해줄 수 있다.

weak로 선언한 변수는 반드시 optional 이어야 한다.

그 이유는 weak로 선언된 변수는 해당 변수를 멤버 변수로 갖고 있는 클래스보다 먼저 메모리에서 해제될 수 있기 때문에, 언제든지 nil이 할당될 수 있기 때문이다.

weak 키워드를 붙임으로써 위의 문제 상황을 해결해보자.

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") }
}

tenant 변수 앞에 weak 키워드가 붙었기 때문에, 이제 tenant 변수가 특정 인스턴스를 참조하더라도 해당 인스턴스의 Reference Count는 올라가지 않는다.

var yeji = Person?
var unit4A: Apartment?

yeji = Person(name: "yeji yun") // Person RC = 1
unit4A = Apartment(unit: "4A") // Apartment RC = 1

위와 같이 똑같이 선언 후 인스턴스들을 할당해주고,

yeji!.apartment = unit4A // Apartment RC = 2
unit4A!.tenant = yeji // Person RC = 1

각각 인스턴스의 apartment, tenant 값에 참조 값을 넣어준다.

위와 다른 점은 unit4A 인스턴스의 tenant 변수가 yeji(Person)를 참조하고 있지만, tenant 변수는 weak로 선언되었기 때문에 yeji(Person)의 reference Count는 증가하지 않는다.

위와 같은 구조가 된다.

여기서 yeji에 nil을 할당하면 어떻게 될까?

yeji = nil // Person RC = 0
// Apartment RC = 1
// Prints "yeji yun is being deinitialized"

약한 참조 변수는 해당 변수가 참조하고 있는 인스턴스가 메모리에서 해제될 때, 참조를 끊는다.

Person Instance의 RC는 1이었고, 1이 0이 되었기 때문에 Person 인스턴스의 apartment 변수가 Apartment 인스턴스를 참조하는 것과 관계 없이 ARC에 의해 메모리에서 해제된다.

  1. yeji 변수가 Person Instance를 strong 하게 참조 ⇒ Person Instance’s RC = 1
  2. Apartment Instance의 tenant 변수가 Person Instance를 weak 하게 참조 ⇒ Person Instance’s RC = 1
  3. yeji 변수에 nil 할당 (더 이상 Person Instance를 참조하지 않음) ⇒ Person Instance’s RC = 0
  4. deinit Person Instance

unit4A 변수만 메모리에 존재하고, unit4A 변수에 nil을 할당하면 Apartment Instance 또한 메모리 상에서 사라짐.

Unowned Reference

weak 참조와 마찬가지로 unowned 참조 또한 Reference Count에 영향을 주지 않는다.

그렇다면 어떤 차이점을 갖고 있는가?

unowned 참조는 참조하고 있는 인스턴스가 현재 unowned 참조를 하는 변수를 멤버 변수로 가지고 있는 클래스보다 먼저 해제되지 않는 경우에 사용한다.

따라서 기본적으로 unowned 참조는 nil 이 될 수 없다.

(→ swift 5부터는 unowned 참조에 optional keyword(→ ?) 를 붙이는 것이 허용됐다. 이 점에 대해서는 추후 포스팅 하도록 하겠다.)

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 클래스와 CreditCard 클래스가 있다.

고객은 신용카드를 가질 수도 있고, 가지지 않을 수도 있지만

신용카드는 한 번 발급된 이상 해당 카드의 소유주(customer)가 존재할 수 밖에 없다.

따라서 customer 변수는 자신보다 더 오래 유지될 수 있는 인스턴스를 참조하는 변수이기 때문에, unowned로 선언된 것이다.

var yeji: Customer?
yeji = Customer(name: "yeji yun")
yeji!.card = CreditCard(number: 1234_5678_9012_3456, customer: yeji!)

yeji yun이라는 이름을 가진 고객 인스턴스가 생성되고, yeji 고객은 card number가 1234_5678_9012_3456을 가지고 있으며,

yeji 고객이 가지는 카드이므로 해당 신용카드의 customer 변수는 yeji yun customer instance를 참조하게 된다.

신용카드는 신용카드로 존재하는 한 반드시 소유주(고객)가 존재한다.

그렇다면 고객에 nil 값을 넣으면 어떻게 될까?

yeji = nil
// Prints "yeji yun is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

  1. yeji는 Customer Instance를 참조하고 있었고, 따라서 Customer RC는 1이 된 상태였다.
  2. CreditCard 인스턴스의 customer 변수는 Customer Instance를 참조하지만 unowned 이므로 여전히 Customer RC는 1인 상태였다.
  3. yeji에 nil을 할당함으로서 Customer RC가 0이 되고, Customer Instance가 메모리에서 해제됨으로써 모든 reference가 끊기게 된다.
  4. 따라서 Customer Instance, CreditCard Instance가 전부 다 deinit 되었다.
profile
나의 언어로 설명할 수 있을 때까지 😎

0개의 댓글