[번역] Retain Cycles, Weak, Unowned in Swift
Memory Management, Retain Cycle 그리고 weak, unowned 키워드의 사용법은 약간은 혼란스러운 주제일 수 있습니다. 반대로 이 주제를 이해하는 것은 매우 중요합니다. 왜냐하면 Retain Cycle은 메모리 문제를 일으키는 주된 요인 중 하나이기 떼문입니다. 하지만 걱정하지마세요! 이 글에서는 당신이 필요한 모든 것을 배우게 될 것입니다.
참고 : 이 글은 Swift3과 ios10으로 업데이트 되었습니다.
Contents
우리는 스위프트에서 메모리 관리에 대한 기본적인 것 부터 시작해볼 것입니다. 우리는 기본적으로 Retain Cycle이 무엇인지, 그리고 weak과 unowned 키워드를 통해서 어떻게 이러한 현상을 피할 수 있는지에 대해 알아볼 것입니다. 그 이후로는 Retain Cycle이 일어나는 가장 흔한 시나리오 두개를 살펴볼 것입니다. 항상 그래왔듯이 모든 단계를 플레이그라운드를 통해 따라해보시는 것을 적극 추천해드립니다.
- How does memory management work in Swift?
- What is a retain cycle?
- weak
- unowned
- Common scenarios for retain cylces : delegates
- Common scenarios for retain cylces : closures
- Detecting retain cycles by using log messages in deinit
- Summary
- References
1. How does memory management work in Swift?
스위프트의 메모리 관리의 기초를 살펴보는 것으로 시작해보겠습니다. ARC ( automatic reference counting ) 는 대부분의 메모리 관리를 당신을 위해 해줍니다. 이건 굉장히 좋은 소식입니다.
원리는 매우 간단합니다. 기본적으로 클래스의 객체를 가리키는 각각의 reference(이하 참조)는 강한참조입니다. 최소한 하나의 강한참조가 있는한 이 객체의 메모리는 해제되지 않을 것입니다. 만일 객체에 대한 강한 참조가 존재하지 않는다면 이는 메모리에서 해제될 것입니다. 다음 예제를 통해 살펴보도록 하겠습니다.
class TestClass {
init(){
print("init")
}
deinit(){
print("deinit")
}
}
var testClass: TestClass? = TestClass()
testClass = nil
객체를 생성한 후에는 참조와 객체의 관계는 다음의 다이어그램의 모양을 띕니다.
testClass
는 TestClass
의 객체에 대한 강한 참조를 갖고 있습니다. 그리고 우리가 만약 참조를 nil
로 할당한다면 그 강한 참조는 사라지고 그로인해 TestClass
의 객체의 메모리는 해제되게 됩니다.
덧붙여 말하면 콘솔창을 확인해보시면 모든 것들이 제대로 작동한다는 것을 아실수 있습니다. 왜냐하면 deinit
메소드는 객체가 메모리에서 해제될 때 호출되기 때문입니다.
init
deinit
만일 TestClass
객체의 메모리가 해제되지 않았다면 deinit
메소드는 호출되지 않을 것입니다. 우리가 나중에 논의해볼 것이지만 deinit
메소드 안에 Log 메시지를 위치시키는 것은 객체의 메모리 해제를 관찰할 수 있는 매우 좋은 방법입니다.
2. What is Retain Cycle?
ARC의 원리는 제대로 작동을 하고 대부분의 경우 여러분은 이에 관해서 생각할 필요가 없습니다. 그러나 이러한 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
를 작성하였습니다. 우리는 TestClass
클래스의 객체 두개를 생성하였고 그 둘은 서로를 가리키고 있습니다. 이러한 관계의 다이어그램은 다음과 같습니다.
그리고 두 변수에 nil
을 할당해봅시다.
testClass1 = nil
testClass2 = nil
하지만 두 객체의 메모리는 해제되지 않았습니다! 당신은 이것을 deinit
메시지가 출력되지 않은 것을 통해 아실 수 있습니다. 무슨 일일까요? 상황을 다이어그램으로 살펴보도록 하겠습니다.
각각의 객체는 강한 참조를 하나씩 잃었습니다. 하지만 각각의 객체는 아직 내부적으로 한개씩의 참조를 갖고 있습니다. 이는 두 객체들의 메모리가 해제되지 않을 것이라는걸 의미합니다. 심지어 더 심각한 것은 이 두 객체에 대한 참조는 우리의 코드에서 더 이상 존재하지 않는다는 것입니다. 즉 이 두 객체의 메모리를 해제하는 방법은 존재하지 않습니다.
이러한 현상을 메모리 누수(Memory Leak)라고 합니다. 만약 여러분의 어플리케이션에 이러한 메모리 누수가 몇군데 발생하게 된다면 사용할 때마다 메모리의 사용량이 증가하게 될 것입니다. 그리고 이러한 메모리 사용량이 높다면 iOS는 당신의 어플리케이션을 Kill 하게 될 것입니다. 이것이 우리가 Retain Cycle을 잘 다뤄야 하는 이유입니다. 그렇다면 어떻게 이러한 현상을 막을 수 있을까요?
3. Weak
소위 말하는 “약한 관계”를 사용함으로써 Retain Cycle을 피할 수 있습니다. 만일 당신이 참조를 weak
으로 선언한다면 이것은 “강한 참조”가 되지 않습니다. 우리의 코드를 바꿔보고 어떤 현상이 일어나는지를 살펴보도록 하겠습니다.
class TestClass{
weak var testClass: TestClass? = nil // 이제 이 참조는 약한 참조이다!
init(){
print("init")
}
deinit(){
print("deinit")
}
}
var testClass1: TestClass? = TestClass()
var testClass2: TestClass? = TestClass()
testClass1?.testClass = testClass2
testClass2?.testClass = testClass1
testClass1 = nil
testClass2 = nil
이러한 자그만 변화를 한 후의 결과를 본다면 우리가 원하던 결과를 얻을 수 있습니다.
init
init
deinit
deinit
밑의 다이어그램은 위와 같은 관계를 보여주는 그림입니다.
오직 약한 관계만이 남아있다면 객체들의 메모리는 해제될 것입니다.
즉 weak reference
는 참조는 할 수 있지만 Reference Count가 증가되지 않습니다.
weak
에 관해 여러분이 알아야할 중요한 점이 있습니다.
객체의 메모리가 해제된 후 그에 대응하는 변수는 자동으로 nil
이 될 것입니다. 이것은 좋은 현상입니다. 왜냐하면 만일 변수가 이미 메모리가 해지된 객체의 영역을 가리키고 있다면 프로그램은 runtime exception
을 발생시키기 때문입니다.. 또한 optional
타입만이 nil
값이 될 수 있기 때문에 모든 weak
참조 변수는 반드시 optional
타입이어야 합니다.
4. Unowned
weak
밖에도 변수에 적용할 수 있는 unowned
라는 옵션이 존재합니다. weak
과 매우 비슷한 역할을 하지만 한가지 예외가 있습니다. unowned
로 선언된 변수는 nil
로 될 수 없습니다. 그러므로 unowned
변수는 optional
로 선언되어서는 절대 안됩니다. 하지만 이전 문단에서 설명드렸듯이 메모리가 해제된 영역의 접근을 시도한다면 어플리케이션은 runtime exception
을 발생시킵니다. 이 뜻은 여러분은 unowned
는 해제된 메모리 영역을 접근하지 않는다는 확실한 경우만 사용해야한다는 뜻입니다.
일반적으로 weak
을 사용하는 것이 보다 안전합니다. 하지만 하지만 여러분이 변수가 weak
이 되길 원하지 않고 또한 해당 변수가 가리키는 객체의 메모리가 해제된 이후에는 해당 영역을 가리키지 않는다는 확신이 있다면 당신은 unowned
를 사용할 수 있습니다.
이는 implicitly unwrapping optional
과 try!
와 같이 사용은 할 수 있으나 대부분의 상황에서는 좋지 않은 방법입니다.
5. Common Scenarios for Retain Cylces : Delegates
그렇다면 Retain Cycle가 일어나는 흔한 시나리오는 어떤 것들이 있을까요? 가장 흔한 시나리오 중 하나가 바로 delegate
의 사용입니다. 여러분의 프로그램에 자식 view controller
를 갖고 있는 부모 view controller
가 있다고 상상해보세요 부모 view controller
는 다음 예제의 상황에서 처럼 특정 상황에서의 정보를 얻기 위해 스스로 본인을 자식 view controller
의 대리자
로 설정할 것입니다.
class ParentViewController: UIViewController, ChildViewControllerProtocol: class{
let childViewController = ChildViewController()
func prepareChildViewController(){
childViewController.delegate = self
}
}
protocol ChildViewControllerProtocol {
// important functions
}
class ChildViewController: UIViewController {
var delegate: ChildViewControllerProtocol?
}
만약 여러분이 이런 방법으로 코드를 작성하신다면 ParentViewController
가 pop된 이후에 발생하는 Retain Cycle로 인해 메모리 누수가 발생하게 됩니다.
대신에 우리는 delegate
프로퍼티를 반드시 weak
으로 선언해야합니다.
weak var delegate: ChildViewControllerProtocol?
또한 만일 여러분이 UITableView
의 정의를 보게 된다면 delegate
와 dataSource
프로퍼티가 weak
으로 선언된 것을 확인하실 수 있습니다.
weak public var dataSource: UITableViewDataSource?
weak public var delegate: UITableViewDelegate?
그러므로 여러분은 delegate
를 선언해야하는 거의 대부분의 상황에서 weak
을 사용함으로써 Retain Cycle을 예방할 수 있습니다.
6. Common Scenarios for Retain Cycles : Closures
Closure
는 Retain Cycle이 빈번히 일어나는 시나리오 중 하나입니다. 다음의 코드를 통해 살펴보도록 하겠습니다.
class TestClass{
var aBlock: (()->())? = nil
let aConstant = 5
init(){
print("init")
aBlock = {
print(self.aConstant)
}
}
deinit(){
print("deinit")
}
}
var testClass: TestClass? = TestClass()
testClass = nil
우리는 Log 메시지를 통해서 TestClass
객체의 메모리가 해제되지 않았음을 알 수 있습니다. 문제는 TestClass
의 객체의 내부에서 Closure
로, Closure
에서 TestClass
객체로 강한 참조를 하고 있기 때문입니다.
Closure
역시Class
와 마찬가지로Reference Type
입니다.
여러분은 이러한 문제를 weak self
를 capture
해줌으로써 해결할 수 있습니다.
class TestClass{
var aBlock: (()->())? = nil
let aConstant = 5
init(){
print("init")
aBlock = { [weak self] in
print(self?.aConstant)
}
}
deinit(){
print("deinit")
}
}
이제 우리는 Log 메시지를 통해 객체의 메모리가 정상적으로 해제되는 것을 확인할 수 있습니다.
하지만 Closure
를 사용한다고 항상 Retain Cycle이 발생하는 것은 아닙니다. 예를 들어 만일 여러분이 Closure
블록을 locally
하게만 사용한다면 self
를 weak
하게 capture
할 필요가 없습니다.
class TestClass {
let aConstant = 5
init() {
print("init")
let aBlock = {
print(self.aConstant)
}
}
deinit {
print("deinit")
}
}
var testClass: TestClass? = TestClass()
testClass = nil
그 이유는 바로 Closure
블록에 대한 강한 참조가 존재하지 않기 때문입니다. 블록 자체는 블록 내부에서 self
, 즉 TestClass
객체를 강하게 참조하지만 Closure
자체는 메소드 지역안에 존재하기 때문에 메소드가 return
되면 메모리에서 해제됩니다. 이와 같은 예제를 UIView.animateWithDuration
에서 찾아볼 수 있습니다.
class TestClass{
let aConstant = 5
init(){
print("init")
}
deinit(){
print("deinit")
}
func doSomething(){
UIView.animate(withDuration: 5){
let aConstant = self.aConstant
// fancy animation ... .
}
}
}
var testClass: TestClass? = TestClass()
testClass?.doSomething()
testClass = nil
그래서 만일 Closure
블록에 대한 강한 참조가 존재하지 않는다면 여러분은 Retain Cycle에 대해 걱정하실 필요가 없습니다.
여러분은 또한 unowned
역시 weak
대신 사용할 수 있고 우리가 사용했던 이전의 예제는 unowned
보다 안전하게 사용이 가능합니다.
7. Detecting retain cycles by using log messages in deinit
Retain Cycle이 무엇인지 그리고 그들을 어떻게 피할수 있는지를 배웠다면 우리는 Retain Cycle을 어떻게 감지하는지에 관해 논의해봐야합니다. 글쓴이가 가장 좋아하는 방법은 deinit
메소드 안에 Log 메시지를 출력하는 코드를 작성하는 것입니다. 겉보기에는 대단한 것 같아 보이지 않지만 이는 실제로 매우 효과적인 방법 중 하나입니다.
deinit(){
print("deinit")
}
만일 우리가 객체의 메모리가 해제되었을 것이라고 기대를 해도 콘솔창에 Log 메시지가 출력이 되지 않는다면 우리는 무엇인가 잘못되었다는 것을 알 수 있습니다. 이는 특히 View Controller
들을 다룰 때 매우 유용합니다. (여러분은 반드시 모든 View Controller
마다 이러한 메소드를 작성해주는 것이 좋습니다.) 예를 들어 우리가 View Controller
를 pop한다면 Log 메시지는 출력이 되어야 합니다. 만일 출력이 된다면 이로써 우리는 프로그램이 잘 돌아가고있음을 알 수 있습니다.
8. Summary
이 글에서 여러분은 스위프트의 기본적인 메모리 관리 기법에 관해 배웠고 Retain Cycle이 무엇인지, 어떻게 여러분이 그것이 발생하는 것을 예방할 수 있는지에 대해 배웠습니다. 게다가 어떻게 그들을 감지하는지에 대해서도 배웠습니다. 이러한 것들은 단순한 것이 아니며 더욱 자세히 이해하기 위해서는 여러분은 이 글을 몇번 더 읽어볼 수도 있습니다. 하지만 이러한 개념들을 여러분의 실제 어플리케이션에 적용한다면 이러한 주제들에 더욱 친숙해지게 될 것입니다.
9. References
- Swift: weak and unowned
- Building Memory Efficient Apps
- The Swift Programming Language - Automatic Refernce Couting