본문 바로가기

iOS

[iOS] Retain Cycle에 대해 알아보자(Feat. Delegate)

Retain Cycles, Weak in Swift

- reference
- https://baked-corn.tistory.com/30

 

스위프트에서 메모리관리에대한 기본적인것부터 알아봅시다. Retain Cycle이 무엇인지, 그리고 Weak키워드를 통해 어떻게 이러한 현상을 피할 수 있는지에 대해 알아보도록 합시다. 그 이후로는 Retain Cycle이 일어나는 가장 흔하다는 시나리오 인 Delegate패턴을 알아봅시다.

# 스위프트에선 메모리 관리를 어떻게하나요 ?

ARC( AutoMatic Reference Counting)은 대부분의 메모리 관리를 당신을 위해해줍니다.
원리는 간단한데 기본적으로 클래스 객체를 가리키는 각각의 reference(참조)는 강한참조입니다. 최소한 하나의 강한참조가 있는한 이 객체의 메모리는 해제되지 않을 것입니다. 만일 객체에 대한 강한 참조가 존재하지 않는다면 이는 메모리에서 해제될 것입니다… 예제를 살펴보자.

class TestClass {
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var testClass: TestClass? = TestClass()
testClass = nil

인스턴스를 생성한 후에 참조와 인스턴스의 관계는 Strong Reference를 가집니다.

만일 testClass에 nil을 할당하면 그 강한 참조는 사라지고 그로인해 testClass의 인스턴스의 메모리는 해제되게 됩니다.

고로 실행결과를 확인해보면 deinit 이 호출된것을 확인해볼 수 있습니다.

# Retain Cycle이 무엇인가 ?

ARC원리는 제대로 작동을 하고 대부분의 경우 이에관해 생각할 필요는 없다고 합니다. 그러나 이러한 ARC가 작동하지 않는 상황이 몇몇 있으며 이는 여러분의 약간의 도움이 필요합니다… 예재를 살펴보자.

class TestClass {
    var value: TestClass? = nil
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var testClass1: TestClass? = TestClass()
var testClass2: TestClass? = TestClass()
testClass1?.value = testClass2
testClass2?.value = testClass1

각각의 인스턴스는 강한 참조를 하나씩 가지며 동시에 내부에있는 프로퍼티가 서로를 가르키는 강한 참조가 추가로 존재합니다.

이상태에서 nil값을 할당해보죠.

testClass1 = nil
testClass2 = nil

//실행 결과값
init
init
 

하지만 두 인스턴스의 메모리는 해제되지않았습니다… 이를 deinit을 통해 확인해볼 수 있어요. 도대체 무슨상황이죠… ?

각각의 인스턴스는 강한 참조를 하나씩 잃었습니다. 하지만 각각의 인스턴스는 아직 내부적으로 한개씩 참조를 갖고 있는데 이는 두 객체들의 메모리가 해제되지 않을 것이라는걸 의미합니다. 심지어 더심각한 것은 두 인스턴스에 대한 참조는 우리의 코드에서 더이상 존재하지 않는다는 것이죠 즉 이 두 인스턴스의 메모리를 해제하는 방법은 존재하지 않습니다.

이러한 현상을 메모리 누수(Memory Leak)라고 합니다. 만약 여러분의 앱에 이러한 메모리 누수가 몇군데 발생하게된다면 사용할때마다 메모리의 사용량이 증가하게될것입니다. 그리고 이러한 메모리 사용량이 높다면 iOS는 당신의 어플리 케이션을 Kill하게 될것입니다. 이것이 우리가 Retain Cycle을 잘 다뤄야하는 이유입니다. 그렇다면 어떻게 이러한 현상을 막을 수 있을까요 ?

# Weak

소위 말하는 "약한 관계"를 사용함으로써 Retain Cycle을 피할 수 있습니다. 만일 당신이 참조를 weak로 선언한다면 이것은 "강한참조"가 되지않습니다. 우리의 코드를 바꿔보고 어떤 현상이 일어나는지 살펴보도록 하죠!

class TestClass {
    // 약한참조!!
    weak var value: TestClass? = nil
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var testClass1: TestClass? = TestClass()
var testClass2: TestClass? = TestClass()
testClass1?.value = testClass2
testClass2?.value = testClass1

testClass1 = nil
testClass2 = nil

//실행결과
init
init
deinit
deinit
 

weak인 약한 참조를 붙이기 전에는 강한참조가 2번이일어났고 각인스턴스에 nil을 할당했을때 내부적인 강한참조하 남은채로 deinit이 되지않았따.

하지만 이번에는 내부적인 인스턴스들이 서로 강한참조가 아니라 약한참조를 하게되어 RC가 증가하지 않습니다.

weak에 관해 알아야할 중요한 점이있는데요
객체의 메모리가 해제된 후 그에 대응하는 변수는 자동으로 nil이 될것입니다. 이것은 좋은 현상입니다. 왜냐하면 만일 변수가 이미 메모리가 해지된 인스턴스의 영역을 가리키고 있다면 프로그램은 runtime exception을 발생시키기때문이죠! 또한 optional타입만이 nil값이 될 수 있기때문에 모든 weak참조 변수는 반드시 optional타입이어야 합니다.

# Retain Cycle이 발생하는 흔한 케이스 1: Delegates

그렇다면 RetainCycle가 일어나는 흔한 시나이로는 어떤게 있을까요 ?
가장 흔한 시나리오 중 하나가 바로 delegate의 사용입니다. 여러분의 프로그램에 자식 view controller를 갖고 있는 부모 view controller가 있다고 생각해보세요
부모 view controller는 다음 예재의 상황에서 처럼 특정 상황에서의 정보를 얻기 위해 스스로 본인을 자식 view controller의 대리자로 설정한 것이다.

class ParentViewController: UIViewController, ChildViewControllerProtocol {
    
    let childViewController = ChildViewController()
    
    func prepareChildViewController() {
        childViewController.delegate = self
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }


}

protocol ChildViewControllerProtocol {
    //
}

class ChildViewController: UIViewController {
    var delegate: ChildViewControllerProtocol?
}
 

만일 여러분이 이런 방법으로 코드를 작성하신다면 ParentViewController가 pop된 이후에 발생하는 Retain Cycle로 인해 메모리 누수가 발생합니다.

ParentViewController 내에 ChildViewController타입의 인스턴스가 있고 childViewController.delegate 를 ParentViewController로 설정했으니 서로를 강한 참조하게 되는것이죠.

이경우에 delegate프로퍼티를 반드시 weak로 선언해야합니다.

weak var delegate: ChildViewController?
 

이렇게 했을때는 ParentViewController내 있는 ChildViewController의 인스턴스는 강한참조를 하지만
ChildViewController.delegate는 약한 참조를 하게 되는것이지요!