강한 참조 순환 문제
순환 참조란 두 가지 이상의 객체가 서로에 대한 강한 참조(Strong Reference) 상태를 가지고 있을 때 발생하며, 순환 참조가 발생하게 되면 서로에 대한 참조가 해제되지 않기 때문에 메모리에서 유지되며 이로 인해 메모리 릭이 발생하게 된다.
이런 순환 참조를 해결하기 위해 약한 참조(weak),
미소유 참조(unowned)
reference를 사용한다.
단일 인스턴스에 대한 참조
인스턴스 끼리 서로가 서로를 강한 참조할 때를 대표적인 예로 들 수 있는데, 이를 강한 참조 순환이라고 한다.
예를 한번 보자
먼저 name
프로퍼티를 이니셜라이저로 가지는 Person
클래스가 있고, 옵셔널 Person?
타입을 가질 수 있는 옵셔널 변수를 정의하고 출력하는 예이다.
import UIKit
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
이것을 출력해보면
var student1: Person?
var student2: Person?
var student3: Person?
print(student1) //nil
print(student2) //nil
print(student3) //nil
student1
, student2
, student3
의 변수 들는 다음과 같은 특징을 가지게 된다는 것을 확인할 수 있다.
student1
,student2
,student3
변수는 옵셔널Person?
타입으로 선언되며nil
값으로 할당됨.Person
인스턴스로 참조되지 않음.
그럼 sutendet1
변수를 Person
인스턴스에 할당해보자면
student1 = Person(name: "Seogun")
//Seogun is being initialized
student1
은 Person
클래스의 이니셜라이저가 호출되어 print("\(name) is being initialized")
를 출력했다. 하지만 deinit
내부의 print
함수가 호출되지 않는 것으로 보아 디이니셜라이저는 호출되지 않았다는 것을 알 수 있다.
이 새로운 인스턴스가 student1
에 할당 되었고, student1
과 강한 참조를 가지게 되었다. Person
인스턴스가 student1
에 대한 강함 참조를 가지게 되면서 ARC는 Person
클래스의 메모리에 유지하고 할당해제 시키지 않게 만들게 된다.
만약 student1
이 가지고 있는 참조를 해제하면 student1
이 가지고 있었던 참조를 해제하면서 Person
인스턴스는 어떠한 참조도 가지고 있지 않게 되며 디이니셜라이저가 호출된다.
student1 = nil
//Seogun is being deinitialized 출력
그리고 하나의 인스턴스에 두 가지 이상 변수를 할당해보도록 하면
student1 = Person(name: "Seogun")
//Seogun is being initialized
student2 = student1
student3 = student1
//메모리 주소
print(Unmanaged.passUnretained(student1!).toOpaque()) //0x0000600003418a40
print(Unmanaged.passUnretained(student2!).toOpaque()) //0x0000600003418a40
print(Unmanaged.passUnretained(student3!).toOpaque()) //0x0000600003418a40
student1
, student2
, student3
모두 같은 메모리 주소를 참조하고 있다는 것을 알 수 있다!
student2
, student3
변수에 Person
인스턴스를 참조하고 있는 student1
을 할당하면서, Person
인스턴스에 세 개의 강한 참조가 걸리게 되며, Person
인스턴스는 자신에게 할당된 세 개의 강한 참조가 해제되기 전까지는 할당 해제되지 않는다.
그리고 student1
과 student2
가 참조 해제 되어도 Person
인스턴스는 여전히 student3
에 대한 강한 참조를 가지고 있기 때문에 할당 해제되지 않는다.
student1 = nil
student2 = nil
print(student1) //nil
print(student3) //옵셔널 Person
마지막으로 student3
의 참조를 해제하게 되면 디이니셜라이저가 호출되면서 모든 참조에서 해제 되게 된다.
student1 = nil
student2 = nil
print(student1) //nil
print(student3) //옵셔널 Person
student3 = nil
//Seogun is being deinitialized
인스턴스에 대한 참조는 여러 개가 생길 수 있고, 해당 인스턴스를 deinit
시키기 위해서는 해당 인스턴스가 가진 모든 참조를 해제시켜야 한다.
클래스 인스턴스 강한 순한 참조
ARC
는 Person
인스턴스에 대한 참조의 개수를 트래킹 하고, Person
인스턴스가 더 이상 필요하지 않게 되어 참조가 0이 된다면 할당 해제시킨다는 것을 알 수 있었다.
그런데 만약 두 클래스 인스턴스가 서로에 대한 참조를 유지하게 된다면?
위에서 인스턴스 끼리 서로가 서로를 강한 참조할 때, 이를 강한 참조 순환이라 한다고 했었다. 강한 순환 참조가 발생하는 경우에 대해서 알아보도록 하자!
class Person {
let name: String
init(name: String) {
self.name = name
}
var room: Room?
deinit {
print("\(name) is being deinitialized!")
}
}
class Room {
let number: String
init(number: String) {
self.number = number
}
var host: Person?
deinit {
print("Room \(number) is being deinitialized!")
}
}
Person
클래스는String
타입의name
프로퍼티와nil
로 초기화된 옵셔널Room?
타입의room
프로퍼티가 있다.Room
클래스는String
타입의number
프로퍼티와nil
로 초기화된 옵셔널Person?
타입의host
프로퍼티가 있다.
여기에 각각 Person?
과 Room?
옵셔널 타입을 가지는 변수를 정의하고 Person
, Room
인스턴스를 할당해보자!
var seogun: Person? = Person(name: "서근") //Person 의 인스턴스 참조 횟수 : 1
var myRoom: Room? = Room(number: "50") //Room 의 인스턴스 참조 횟수 : 1
이제 Person
에 room
을, Room
에는 host
를 할당해보면
seogun?.room = myRoom // Room 인스턴스 참조 횟수 : 2
myRoom?.host = seogun // Person 인스턴스 참조 횟수 : 2
이렇게 각 인스턴스의 room
, host
프로퍼티에 값을 저장했고, 인스턴스 간의 참조는 다음과 같다.
Person
의 인스턴스는 Room
인스턴스에 대해 강한 참조를 가지게 되었고, Room
의 인스턴스는 Person
인스턴스에 대해 강한 참조를 가지게 되어 두 인스턴스 사이에 강한 참조 사이클이 생성됐다.
각 변수에 할당된 참조횟수는 다음과 같다.
seogun
(총 2회 참조)Person
의 인스턴스를 강한 참조 하며 reference count 증가 ( 참조 횟수 : 1)Person
의 인스턴스가Room
인스턴스에 대해 강한 참조를 가지게 되며 reference count 증가 ( 참조 횟수 : 2)myRoom
(총 2회 참조)Room
의 인스턴스를 강한 참조 하며 reference count 증가 ( 참조 횟수 : 1)Room
의 인스턴스가Person
인스턴스에 대해 강한 참조를 가지게 되며 reference count 증가 ( 참조 횟수 : 2)
이 두 번수에 nil
을 할당해 참조에서 해제해보게 된다면
seogun = nil // Person 인스턴스의 참조 횟수 : 1
myRoom = nil // Room 인스턴스의 참조 횟수 : 1
seogun
을 완전히 참조에서 해제하려고 했지만,Room
인스턴스가seogun
에 대한 강한 참조를 유지하고 있으므로 메모리에서 사라지지 않고Person
의 인스턴스 참조 횟수가1
로 낮아진다.myRoom
을 완전히 참조에서 해제하려고 했지만,Person
인스턴스가myRoom
에 대한 강한 참조를 유지하고 있으므로 메모리에서 사라지지 않고Room
의 인스턴스 참조 횟수가1
로 낮아진다.
위와 같이 서로에 대한 강한 참조로 인해 인스턴스를 제대로 해지할 수 없는 상태가 강한 순환 참조(strong retain cycle) 상태이다.
그럼 메모리에서 완전히 해제 하려면 어떻게 해야 할까?
이럴 때 강한 참조 순환 문제를 수동으로 해결 가능하다. (비추천 이유는 밑에서)
seogun?.room
이 Room
인스턴스에 대해 강한 참조를 유지 중이고, myRoom?.host
가 Person
의 인스턴스에 대해 강한 참조를 유지 중이니까 이 부분을 먼저 nil
해주고, 그 뒤에 seogun
과 myRoom
도 nil
을 할당해주면 디이니셜라이저가 호출된다.
var seogun: Person? = Person(name: "서근") // Person 인스턴스의 참조 횟수 : 1
var myRoom: Room? = Room(number: "50") // Room 인스턴스의 참조 횟수 : 1
seogun?.room = myRoom // Room 인스턴스의 참조 횟수 : 2
myRoom?.host = seogun // Person 인스턴스의 참조 횟수 : 2
seogun?.room = nil // Room 인스턴스의 참조 횟수 : 1
myRoom?.host = nil // Person 인스턴스의 참조 횟수 : 1
seogun = nil // Person 인스턴스의 참조 횟수 : 0
// 서근 is being deinitialized!
myRoom = nil // Room 인스턴스의 참조 횟수 : 0
// Room 50 is being deinitialized!
변수 또는 프로퍼티에 nil
을 할당하면 참조 횟수가 감소한다는 규식을 생각해보면 위와 같은 방법으로 해결할 수 있을지도 모른다.
하지만 만약 실수로 코드를 빼먹거나 아니면 해제해야할 프로퍼티가 너무 많거나 귀찮다면???
이럴 때!!! 약한 참조와 미소유 참조를 통해 더 명확한 해결책을 찾을 수 있다.
ARC
는 이러한 강한 순환 참조에 대한 메모리 관리를 해주지 않기 때문에 약한 참조(weak reference), 미소유 참조(unowned reference)를 사용해서 해결해야 한다.
읽어주셔서 감사합니다 🤟
'SWIFT > Grammar' 카테고리의 다른 글
Swift : 고급문법[ARC 메모리 관리4 - 미소유 참조(unowned)] (0) | 2022.02.19 |
---|---|
Swift : 고급문법 [ARC 메모리 관리 3 - 약한참조(weak)] (0) | 2022.02.13 |
Swift : 고급문법 [ARC 메모리 관리 1 - 강한 참조] (0) | 2022.02.10 |
Swift: 기초문법 [데이터 타입 - Int와 UInt (feat.카멜케이스)] (4) | 2022.02.09 |
Swift: 주요 함수 [max, min, numericCast, precondition, preconditionFailure, print] (0) | 2022.02.08 |