iOS/Swift

[Swift] weak, unowned, Retain cycle 톺아보기

누알라리 2020. 12. 10. 12:35

개요

 

코드를 작성하다보면 이젠 정말 무의식적으로 클로저안에 [weak self] , [unowned self] 를 사용하는데요.

처음 이 키워드들에 대해 이해했을 때 메모리 사이클에서 순환 참조가..메모리릭이...하면서 대충 이해하고 넘어갔던적이 있어 이 기회에 제대로 정리를 해보려 합니다.

마침 좋은 글이 영문글이 있어 그대로 번역하면서 이해하도록 하겠습니다!

 

출처: www.thomashanning.com/retain-cycles-weak-unowned-swift/

 

Retain Cycles, Weak and Unowned in Swift

Retain cycles and the usage of the keywords weak and unowned are a little bit confusing. In this article you'll learn everything you need to know.

www.thomashanning.com


메모리 관리, retain cycle와 weak, unowned 키워드의 사용은 조금 복잡합니다.

하지만 retain cycle은 메모리 이슈의 매우 중요한 부분 중 하나이므로 이를 제대로 이해하는건 매우 중요한 일입니다.

하지만 걱정마세요~ 이 글을 읽으면 이해할 수 있습니다!

(믿음직)


1. Contents

 

스위프트의 메모리 관리의 기초부터 시작하겠습니다.

이를 기초로, retain cycle이 무엇인지, 그리고 어떻게 weak과 unowned 키워드를 사용해서 이를 피할 수 있는지 배울 것 입니다.

그 후에, retain cycle이 일어나는 2가지 시나리오를 살펴볼것 입니다.

우리는 이 기사를 retain cycle을 감지하는 두 가지 방법으로 결론지을 수 있을 것 입니다.

 


2. How does memory management work in Swift?

 

스위프트의 메모리 관리 기초에 대해 알아보겠습니다.

ARC (automatic refrence counting) 은 사용자를 위해 대부분의 메모리 관리를 해줍니다.

원리는 매우 간단합니다.

기본적으로 class의 instance를 가리키는 각 reference(참조)들은, strong reference(강한 참조)라고 할 수 있습니다.

인스턴스를 가리키는 strong reference가 하나라도 있는 이상, 이 인스턴스는 할당 해제(deallocate)가 되지 않습니다.

인스턴스를 가리키는 strong refrence가 더이상 없을 때만 그 인스턴스는 deallocated 됩니다.

 

예시를 볼까요?

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

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

Test Class 인스턴스가 생성된 후에, 상황은 이렇습니다.

 

testClass는 TestClass 클래스의 인스턴스를 향한 strong reference를 갖습니다.

만일 이 reference를 nil로 세팅한다면, strong reference가 없어지고 남은 strong reference가 없기 때문에 TestClass의 인스턴스는 deallocate 됩니다.

 

또한 콘솔로그에서도 TestClass의 인스턴스가 deallocate 됐을 때 시스템에서 deinit 함수가 호출된것을 확인할 수 있습니다.

TestClass의 인스턴스가 deallocate 되지 않았다면, deinit은 절대 불리지 않았을 겁니다.

deinit 함수에 로그를 심어 오브젝트의 deallocation을 추적하는건 매우 좋은 방법 입니다.

 


3. What is a retain cycle(순환 참조)?

따라서 ARC(Automatic Reference Counting)의 원리는 매우 잘 작동하며, 대부분의 경우 사용자가 이에 대해 생각할 필요는 많이 없습니다.

그러나, ARC가 제대로 작동하지 않아 사용자가 도움을 줘야하는 상황들이 존재합니다!!

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

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

testClass1?.testClass = testClass2
testClass2?.testClass = testClass1

TestClass의 2개의 인스턴스를 생성하고, 각 인스턴스들이 서로를 가리키게 해봅시다!

이 상황을 비쥬얼라이징하면...

 

이렇습니다.

그 다음에 이렇게 하면 어떻게 될까요?

testClass1 = nil
testClass2 = nil

하지만 두 인스턴스는 deallocate 되지 않습니다!

( 이것은 deinit의 콘솔로그로 확인할 수 있습니다. )

현재 상황은 이렇습니다.

 

각 클래스는 하나의 strong reference는 잃었지만, 여전히 하나가 남아있습니다!

이것은 이 인스턴스가 deallocate 되지 않을것이란 의미입니다.

더 난감하게도, 우리는 더이상 코드에서 이 클래스에 대한 reference를 찾을 수 없습니다! (ㅠㅠ)

이를 메모리 누수(memory leak)라고 합니다.

만일 우리의 App에 몇개의 memory leaks가 존재한다면, App의 메모리 사용량은 App을 사용할 때 마다 매우 상승할 것 입니다.

메모리 사용량이 너무 높으면, iOS는 App을 죽입니다(!!) 

따라서 retain cycle을 신경써야 하는건 매우 중요한 문제인 것입니다.그럼 어떻게 memory leak을 막을 수 있을까요?

 


4. weak

weak reference (약한 참조) 를 사용하는 것은 retain cycle을 피할 수 있는 방법 중 하나 입니다.

만일 사용자가 reference를 weak으로 선언한다면, 이것은 strong reference가 아닙니다.

이것은 이 reference가 인스턴스의 deallocate를 방해하지 않는다는 의미입니다.

코드를 변경하고 봅시다!

class TestClass {

	weak var testClass: TestClass? = nil // now this is a weak reference!
    
    init() {
    	print("init!")
    }
    
    deinit() {
    	print("deinit!")
    }
}

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

testClass1?.testClass = testClass2
testClass2?.testClass = testClass1

testClass1 = nil
testClass2 = nil

 

weak 만 붙였을 뿐인데, 우리는 이 콘솔로그를 확인할 수 있습니다.

init
init
deinit
deinit

이 상황을 비쥬얼라이징하면....

weak reference (약한 참조) 들만 남겨놓고 인스턴스들은 모두 deallocate 되었습니다!

 

우리가 weak 키워드에 대해 꼭 알아야할 중요한 것이 있습니다!

인스턴스를 deallocate 하고 나면, 해당 변수(variable)에 대한 응답은 nil이 될 것 입니다.

이것은 매우 좋은것입니다. 왜냐면 만일 우리가 아무 인스턴스도 남지 않은 곳을 가리키는 변수에 접근하려하면, runtile exception을 발생시키기 때문입니다.

옵셔널 변수만이 nil이 될 수 있기 때문에, 모든 weak이 붙은 변수들은 반드시 옵셔널이어야 합니다.

 


5. unowned

weak 뿐만 아니라, 변수에 적용할 수 있는 두번째 modifier가 존재합니다. 바로 unowned 입니다!

하나빼고 전부 weak과 같습니다.

unowned가 붙은 변수는 절대 nil이 될 수 없습니다. 따라서 unowned가 붙는 변수는 절대 옵셔널이여선 안됩니다.

저번 문단에서도 말했듯, App은 우리가 deallocate된 인스턴스에 접근하려하면 runtime에서 crash를 발생시킵니다.

이것은, 사용자는 unowned를 사용자가 인스턴스가 deallocate 된 후에 그곳에 접근하지 않는 변수라고 확신할 수 있는 변수에만 사용해야 한다는 것을 의미합니다.

 

일반적으로 항상 weak을 사용하는게 더 안전하다 말합니다.

하지만, 만일 사용자가 변수를 weak으로 만들고 싶지 않고 인스턴스가 deallocate 된 후에 접근하는 변수가 아님을 확신할 때는 unowned를 사용해도 됩니다.

 

이건 옵셔널 변수를 언래핑할 때 강제 해제 ! 를 사용하는 것과 비슷합니다. -> 근데 이건 별로 추천하는 방법이 아닙니다.

 


6. Common scenarios for retain cycles: delegates

 

그럼 retain cycle을 일으키는 주요 시나리오는 무엇일까요?

첫번째 매우 흔한 경우는 delegate의 사용입니다.

 

자식 VC를 가진 부모 VC를 상상해봅시다. 부모 VC는 자신을 자식 VC의 delegate로 설정했습니다.

class ParentViewController: UIViewController, ChildViewControllerProtocol {
	
    let childViewController = ChildViewController()
    
    func prepareChildViewController() {
    	childViewController.delegate = self
    }
}

protocol ChildViewControllerProtocol: Class {
	// important functions...
}

class ChildViewController: UIViewController {
	var delegate: ChildViewControllerProtocol? 
}

 

만일 이렇게 한다면, 부모VC를 popping 한다면 retain cycle(순환 참조)이 일어나기 때문에 memory leak이 발생합니다!

( 부모 VC가 nil이 된다면, delegate에 접근할 수 있는게 없어지니까요 ㅠㅠ)

 

하지만, 이 경우에 우리는 delegate 프로퍼티를 weak으로 선언할 수 있습니다!

weak var delegate: ChildViewControllerProtocol?

이러면 순환 참조가 풀리게되고, memory leak이 일어나지 않게되겠죠.

 

UITableView의 정의를 보면, delegate와 datasource 프로퍼티가 weak으로 선언되어있는것을 확인할 수 있습니다!

weak public var dataSource: UITableViewDataSource?
weak public var delegate: UITableViewDelegate?

기억하세요!

delegate를 선언하는 모든 경우에 순환 참조(retain cycle)을 방지하기 위해 weak 키워드를 써줘야 합니다!

 

7. Common scenarios for retain cycles: closures

클로저는 순환 참조가 매우 잘 발생하는 다른 시나리오 입니다!

class TestClass {
	
    var aBlock: (() -> ())? = nil
    
    let aConstant = 5
    
    init() {
    	print("init")
        aBlock = {
        	print(self.aConstant)
        }
    }
    
    deinit {
    	print("deinit")
    }
}

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

이 코드를 실행하면, 콘솔로그에 TestClass가 deallocate 되지 않는것을 확인할 수 있습니다.

문제의 원인은, TestClass가 클로저에 strong reference를 갖고 있고 클로저는 TestClass에 대해 strong reference를 갖고 있기 때문입니다.

 

이미지에 오타난듯. 밑의 weak -> strong 입니다!

사용자는 이 문제를 self 참조를 weak으로 capturing 함으로써 해결할 수 있습니다!

class TestClass {

	var aBlock: (() -> ())? = nil
    
    let aConstant = 5
    
    init() {
    	print("init")
        aBlock = { [weak self] in // self is captured as weak!!!!
        	print(self?.aConstant)
        }
    }
	
    deinit {
    	print("deinit")
    }
}

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

이제 순환 참조가 풀렸기 때문에 deallocate가 제대로 이루워져 deinit이 잘 찍히는걸 확인할 수 있습니다!

 

근데, 클로저를 사용할 때 항상 순환참조가 발생하는건 아닙니다!

예를들어, 만일 local 에서 클로저를 사용했다면, self를 굳이 weak 으로 캡쳐할 필요는 없습니다!

class TestClass {
	
    let aConstant = 5
    
    init() {
    	print("init")
        
        // local using block
        
        let aBlock = { 
        	print("self.aConstant")
        }
    }

	deinit {
    	print("deinit")
    }
}

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

이유는, Block을 향한 strong reference가 없기 때문입니다. 그래서, 블락은 init 함수가 return 될 때 자동으로 deallocate 됩니다.

 

이것은 UIView.animationWithDuration에서도 똑같습니다.

class TestClass {
	
    let aConstant = 5
    
    init() {
    	print("init")
    }
    
    deinit {
    	print("deinit")
    }

	func doSomething() {
    	UIView.animate(withDuration: 5) {
        	let aConstnat = self.aConstant
            
            // fancy animations....
        }
    }
}

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

그래서 만약 블락에 대한 strong reference가 없다면, 사용자는 순환 참조에 대해 걱정할 필요가 없습니다!

 

사용자는 물론 weak 대신에 unowned를 사용할 수 있습니다.

그리고 더 안전하게 하기위해 위의 예제들에 weak, unowned 캡쳐를 해도 됩니다.

 

class TestClass {
    
    var aBlock: (() -> ())? = nil
    
    let aConstant = 5
    
    
    init() {
        print("init")
        aBlock = { [unowned self] in
            print(self.aConstant)
        }
    }
    
    deinit {
        print("deinit")
    }
    
    
}
 
var testClass: TestClass? = TestClass()
testClass = nil

더 안전하게 할 수 있습니다. 왜냐면 TestClass가 deallocate되면 블락도 그럴테니까요.

블락이 deallocate되었을 때 TestClass에 접근할 일은 없습니다. 하지만, 필자의 의견으로는 모든 상황에서 weak을 쓰는 연습을 하는게 좋다고 생각합니다! weak을 쓰면 옵셔널 바인딩 때문에 귀찮긴 하지만, 항상 더 안전하니까요!

'iOS > Swift' 카테고리의 다른 글

Reduce, Map, Filter  (0) 2021.03.28
Swift로 함수형 프로그래밍 시작하기  (2) 2021.03.28
[Swift] lazy 키워드  (0) 2020.12.20