[Swift] ARC 2편 - Strong Reference Cycle

어흥·2024년 6월 17일

Swift

목록 보기
20/28

Strong Reference Cycle

객체가 서로를 참조하여 변수의 참조에 nil을 할당해도 메모리가 해제가 되지 않는 참조 사이클

  • 메모리 누수(Memory Leak)의 상황이 발생
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(name: "John Appleseed")
var unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

강한 순환 참조

  • john 인스턴스의 apartment 프로퍼티로 unit4A를 가리킴 → Person 인스턴스 reference count: 2
  • unit4A 인스턴스의 tenant 프로퍼티로 john를 가리킴 → Apartment 인스턴스 reference count: 2

여기서 다시 ARC의 메모리 관리 방식에 대해서 기억 필요

  • 객체를 가리키는 소유자가 하나라도 있다면 메모리에 해제되지 않는다.
john = nil
unit4A = nil

john = nil 코드 실행 후, Person 인스턴스 reference count 2 → 1

unit4A = nil 코드 실행 후, Apartment 인스턴스 reference count 2 → 1

→ 참조 카운트가 1이므로 메모리에 해제되지 않는다.
이렇게 되면 메모리가 해제되지 않고 해제할 방법도 없음 → 메모리 누수 발생

Strong Reference Cycle 해결 방안

강한 참조 사이클로 인한 메모리 누수를 해결할 방안은 2가지다.

🔥 weak, unowned 키워드를 사용하는 것이다.

해당 키워드를 사용하면 가르키는 인스턴스의 reference count가 올라가지 않는다. → 강한 참조하지 않음

  • 말그대로 weak/unowned는 강한 참조가 아니기 때문에 인스턴스에 접근은 가능하지만 메모리에 인스턴스를 유지시키는 것은 불가능함

Weak Reference(약한 참조)

weak로 선언하면 메모리에 인스턴스를 유지시키지 못 함

  • 참조하는 인스턴스가 할당 해제되면 약한 참조된 변수를 nil로 자동 설정
  • 언제든지 nil로 변할 수 있으므로 weak 프로퍼티는 옵셔널 타입 변수로 선언되어야 함

Note
프로퍼티 관찰자는 ARC가 약한 참조로 설정할 때 호출되지 않는다.

  • 아래 코드를 실행해보면 weak 프로퍼티는 observer가 실행되지 않음을 알 수 있다.
class Person {
    let name: String
    init(name: String) { self.name = name }
    weak var apartment: Apartment? {
			    willSet {
            print("현재 이름 = \(owner?.name), 바뀔 이름 = \(newValue)")
            }
            didSet {
                print("현재 이6름 = \(owner?.name), 바뀌기 전 이름 = \(oldValue)")
            }
   }
    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(name: "John Appleseed")
var unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

예시

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? // weak 프로퍼티로 선언됨
    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

두 인스턴스는 아래 그림과 같은 참조 관계를 갖는다.

  • Apartment 클래스에서 Person 인스턴스를 가리키는 변수를 weak로 선언 → 강한 참조 사이클이 되지 않음
  • Person 인스턴스 reference count: 1, Apartment 인스턴스 reference count: 2

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

위와 같이 john 인스턴스에 nil을 할당하면 Person 인스턴스 reference count는 0이 되어 메모리에서 해제된다. 따라서 Apartment 인스턴스를 가리키는 참조 하나도 자연스레 없어진다.

unit4A까지 nil을 할당하면 남아있는 Apartment 인스턴스 참조가 0이 되면서 메모리에서 해제됩니다.

그렇게 자연스레 메모리 누수를 방지할 수 있습니다.

unit4A = nil
// Prints "unit4A Appleseed is being deinitialized"

Unowned Reference (미소유 참조 )

unowned는 weak 보다 다른 인스턴스의 수명이 같거나 더 긴 경우 사용

unowned reference는 항상 값을 갖도록 예상 → 참조하고 있던 인스턴스가 사라지면, nil로 초기화 되지 않음

  • Swift 5 버전이후, 옵셔널로 선언하는 것 가능하지만 nil 자동 할당하지 않음
class Dog1 {
    var name: String
    unowned var owner: Person1?  
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person1 {
    var name: String
    unowned var pet: Dog1?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
    
var bori1: Dog1? = Dog1(name: "보리1")
var gildong1: Person1? = Person1(name: "홍길동1")

// 강한 참조 사이클이 일어나지 않음
bori1?.owner = gildong1
gildong1?.pet = bori1
  • gildong1이 해제되면 refenece count가 0이되므로 메모리 해제됨
    • 하지만 owner은 owned reference이므로 가르키고 있는 인스턴스가 해제되도 nil로 할당되지 못함 → 에러 발생
    • 에러가 발생하지 않게 nil로 재설정 필요 bori1?.owner = nil
// 에러발생하는 케이스

gildong1 = nil
bori1?.owner   // nil로 초기화 되지 않음 에러 발생

주의할 점

weak

  • optional type으로만 선언 가능
  • non-optional type 불가 → 인스턴스가 해제되면 nil로 할당되어야 함
  • var로만 선언가능 (nil로 변경), let 불가능

unowned

  • non-optional, optional 모두 가능 (swift 5.3부터 optional 타입 가능)
  • let, var 모두 가능

0개의 댓글