순환 참조란
순환 참조란 여러 클래스 인스턴스가 서로간에 강한 참조상태(Strong Reference)를 가질 때 발생하고, 순환 참조가 발생하게되면 서로간의 참조가 해제되지 않기때문에 메모리 누수(Leak)가 발생할 수 있다.
순환 참조 예
class Person {
var name: String
var puppy: Puppy?
init(name: String) {
self.name = name
}
deinit {
print("Person deinit")
}
}
class Puppy {
var name: String
var owner: Person?
init(name: String) {
self.name = name
}
deinit {
print("Puppy deinit")
}
}
var john: Person? = Person(name: "John")
var jackson: Puppy? = Puppy(name: "Jackson")
john?.puppy = jackson
jackson?.owner = john
위 그림처럼 여러 클래스 인스턴스가 서로를 참조하고 있는 상황을 순환참조 상태 라고 한다.
해당 그림엔 명시되있지 않지만, 모두 강한 참조를 하고있는 상태이다.
위의 코드의 클래스 인스턴스 프로퍼티를 보면 변수로만 정의되어 있다. 이는 Strong이 생략된 상태이다.
따로 정의한 키워드가 없다면 default값은 Strong이다.
강한 순환 참조
위의 그림으로 계속.. 써먹겠다.
그림 그리기 힘들..어..요..
강한 참조의 경우 클래스 인스턴스의 주소값이 할당될때, 즉 클래스 인스턴스가 사용될 때 RC값이 오른다.
보통 같은 경우엔 nil을 할당하여 클래스 인스턴스의 사용을 하지않게 하면 RC값이 내려가고 최종적으로 0이되면 deinit이 호출되어 정상적으로 메모리에서 해제가 된다.
하지만 순환 참조에선 상황이 다르다.
위의 그림이 예시다. 서로의 클래스 인스턴스 프로퍼티가 다른 클래스 인스턴스를 참조하고 있을 땐 문제가 발생한다.
클래스 인스턴스를 할당받은 변수에 nil을 할당해도 deinit이 호출되지 않는 문제가 발생할 수 있다.
이말인 즉슨 정상적으로 클래스 인스턴스가 메모리에서 해제가 되지않는다는 것이다.
위의 순환 참조 코드에서 클래스 인스턴스를 참조하고 있는 변수에 nil을 할당하여 클래스 인스턴스를 참조 해제 시켜보았다.
아직 클래스 인스턴스가 메모리에서 해제되지 않아 deinit이 호출되지 않는다.
클래스 인스턴스의 프로퍼티끼리 서로를 참조하는데 이것들이 있으므로 RC가 0이 되지않아 deinit을 호출하지 않고, 정상적으로 메모리에서 해제되지 않아 메모리 누수가 발생하고 있는 것이다.
이를 해결할 방법 또한 현재로선 없다.
왜냐하면 해당 클래스 인스턴스를 참조하여 프로퍼티에 접근할 수 있던 변수에 nil을 넣어 메모리에서 내려버렸기 때문에 접근조차 할 수 없는 것이다.
그럼 프로그램을 종료시키기 전까진 계속 쓸데없는 곳에 메모리를 소모하고 있는 꼴이 된다.
이런 문제를 강한 참조 순환이라고 한다.
그럼 강한 참조 순환이 발생하기 전에 이를 방지해야되는데.. 어떤 방법이 있을까?
약한 참조 (Weak)
여러 방법중 약한 참조 라는 방법이 있다.
변수, 프로퍼티 앞에 weak 키워드를 붙임으로써 약한 참조임을 선언할 수 있고, 이는 참조하는 클래스 인스턴스에 대해 아까 문제가 발생헀던 강한 참조를 하지 않아 ARC가 참조 해제를 정상적으로 시킬 수 있어 강한 참조 순환을 방지할 수 있다.
강한 참조 순환 방지할 수 있는 이유는 다음과 같다.
- 클래스 인스턴스를 참조해도, RC를 증가시키지 않는다.
- 서로 참조하던 클래스 인스턴스중 하나가 메모리에서 해제되면, 다른 클래스 인스턴스에도 자동으로 nil이 할당되어 메모리에서 해제된다.
- 클래스 인스턴스 프로퍼티가 다른 클래스 인스턴스를 참조하던 도중 nil이 할당될 수 있으니 Optional타입이여만 한다.
예를 보자
class Person {
var name: String
weak var puppy: Puppy?
init(name: String) {
self.name = name
}
deinit {
print("Person deinit")
}
}
class Puppy {
var name: String
var owner: Person?
init(name: String) {
self.name = name
}
deinit {
print("Puppy deinit")
}
}
var john: Person? = Person(name: "John")
var jackson: Puppy? = Puppy(name: "Jackson")
john?.puppy = jackson
jackson?.owner = john
위 예제코드와 그림을 보자
순환 참조 상태이다.
지역변수 john, jackson이 각각 Person, Puppy를 참조하고 있다.
Puppy 인스턴스 프로퍼티는 Person을 강하게 참조하고 있어 Person의 RC는 카운트 되지만 Person 인스턴스 프로퍼티는 Puppy을 약하게 참조하고 있으므로 Puppy의 RC는 카운트 되지 않는다.
john = nil
jackson = nil
클래스 인스턴스를 참조하고 있던 지역변수에 nil을 할당하였다.
그러므로 각 클래스 인스턴스에 RC -1
클래스 인스턴스를 참조하고 있던 지역변수에 nil을 할당함으로써, Puppy의 RC는 0이 되어 먼저 메모리에서 해제된다.
서로 참조하던 클래스 인스턴스중 하나가 메모리에서 해제되면, 다른 클래스 인스턴스에도 자동으로 nil이 할당되어 메모리에서 해제된다.
위에서 말했듯이 서로 참조하고 있던 Person과 Puppy중 하나가 먼저 메모리에서 해제됐으므로 ARC는 먼저 간 클래스를 참고하고 있던 클래스 인스턴스 프로퍼티에 nil을 할당하여 메모리에서 해제 시킨다.
결과
순환참조 상태였던 것들이 정상적으로 두 클래스 모두 메모리에서 해제되었다.
unowned까지 쓸랬드만 넘 길어져서 다음 글에서 써야겠다.
사실 힘듦
'iOS > Swift' 카테고리의 다른 글
Swift) App LifeCycle (0) | 2022.02.13 |
---|---|
Swift) weak, unowned (0) | 2022.02.13 |
Swift) ARC (0) | 2022.02.13 |
Swift) Protocol(1) (0) | 2022.02.13 |
Swift) removeAll() vs [] (0) | 2022.02.13 |
댓글