안녕하세요 🐶
빈 지식 채우기의 비니🙋🏻♂️ 입니다.
오늘은 ARC의 두 번째 시간입니다!
Retain Cycle과 참조 종류 3가지에 대해 알아보는 시간을 가지겠습니다.
1. 개요
저번 글에서는 ARC의 기본 개념과 Reference Count에 대해 알아보았습니다.오늘은 ARC가 제대로 작동할 수 없게 되는 이유에 대해 알아보고, 해결할 수 있는 방안에 대해 공부해보도록 하겠습니다.
ARC의 개념에 대해 잘 모르시는 분은 아랫 글을 참고하면 감사드리겠습니다!
2. 메모리 누수 - Retain Cycle ( 강한 순환 참조 )
ARC는 자동으로 메모리 관리를 해주고 참조 카운트를 통해 적절하게 메모리를 해제시켜준다.
메모리 참조는 크게 강한참조, 약한참조, 미소유참조 가 있다.우리가 기본적으로 상수, 변수 등 프로퍼티를 선언할 때 별도의 식별자를 작성하지 않으면 기본으로 강한참조를 하게 된다.
흔히 사용하는 var, let 이런 것이 다 강한참조 였었다..두둥!
그런데 여러개의 참조를 발생하는 상황에서, 강한 참조를 잘못 사용하면 메모리 누수 문제가 발생할 수 있다.
이 문제를 바로 강한 순환 참조 Retain Cycle 이라고 한다.
대표적으로 인스턴스 내 프로퍼티가 서로를 강한 참조할 때가 대표적으로 일어난다!
아래에 Retain Cycle이 일어나는 대표적인 사례를 작성해 보았다.
class RC_Test1 {
var A: RC_Test2? // (1)
init() {
print("RC_Test1 Init")
}
deinit() {
print("RC_Test1 deInit")
}
}
class RC_Test2 {
var A: RC_Test1? // (1)
init() {
print("RC_Test2 Init")
}
deinit() {
print("RC_Test2 deInit")
}
}
var RC1 = RC_Test1() // (2)
var RC2 = RC_Test2()
RC1.A = RC2 // (3)
RC2.B = RC1
RC1 = nil // (4)
RC2 = nil
- RC_Test1, RC_Test2 클래스에는 각 클래스를 참조하는 프로퍼티가 존재한다.
- 이때 각각의 클래스에 대해 인스턴스를 생성한다.
- 현재 각 인스턴스의 참조 카운터
- RC_Test1 : 1
- RC_Test2 : 1
- 현재 각 인스턴스의 참조 카운터
- 추가로, 두 인스턴스의 각각 프로퍼티에 서로의 클래스를 참조하도록 하였다.
- 현재 각 인스턴스의 참조 카운터
- RC_Test1 : 2
- RC_Test2 : 2
- 현재 각 인스턴스의 참조 카운터
- 그 후, RC1과 RC2를 동시에 nil로 할당한다
- 현재 각 인스턴스의 참조 카운터
- RC_Test1 : 1
- RC_Test2 : 1.
- 현재 각 인스턴스의 참조 카운터
4번 결과 이후에 문제가 발생하게 된다.
1) RC1과 RC2를 동시에 nil로 할당하였다.
2) 각각의 인스턴스를 접근할 수 있는 경로가 사라졌다.
3) nil 할당으로 참조 카운터가 1이 감소하지만, 더 이상 감소시킬 수 있는 방법이 없다.
따라서 RC1과 RC2 인스턴스 모두 참조 카운터가 1로 남게되고
새로운 참조 카운터를 감소 시킬 수 있는 방법이 없기 때문에 메모리에 계속 남게 된다.
이것이 바로 메모리 누수이다.
이 문제를 해결하기 위해 우리는 아래와 같은 방법을 사용할 수는 있다.
RC1.A = nil
RC1 = nil
RC2.B = nil
RC2 = nil
다만 이러한 방법은 개발자 실수가 일어날 수 있고, 복잡한 클래스의 경우는 해결하기 어려울 것이다.
이때 나온 방법이 약한 참조와 미소유 참조이다.
3. Retain Cycle 해결 (1) - Weak Reference ( 약한 참조 )
약한 참조는 참조하는 인스턴스의 참조 카운트를 증가시키지 않는다.
약한 참조가 어떻게 쓰고, 어떻게 동작하는지 확인해보자.
class RC_Test1 {
var A: RC_Test2? // (1)
init() {
print("RC_Test1 Init")
}
deinit() {
print("RC_Test1 deInit")
}
}
class RC_Test2 {
weak var A: RC_Test1? // (1)
init() {
print("RC_Test2 Init")
}
deinit() {
print("RC_Test2 deInit")
}
}
var RC1 = RC_Test1() // (2)
var RC2 = RC_Test2()
RC1.A = RC2 // (3)
RC2.B = RC1
RC1 = nil // (4)
RC2 = nil
- RC_Test1, RC_Test2 클래스에는 각 클래스를 참조하는 프로퍼티가 존재한다.
- 이때 각각의 클래스에 대해 인스턴스를 생성한다.
- 현재 각 인스턴스의 참조 카운터
- RC_Test1 : 1
- RC_Test2 : 1
- 현재 각 인스턴스의 참조 카운터
- 추가로, 두 인스턴스의 각각 프로퍼티에 서로의 클래스를 참조하도록 하였다.
- 현재 각 인스턴스의 참조 카운터
- RC_Test1 : 1 ( weak 으로 인해 참조 카운트 증가 X )
- RC_Test2 : 2
- 현재 각 인스턴스의 참조 카운터
- 그 후, RC1과 RC2를 동시에 nil로 할당한다
- 현재 각 인스턴스의 참조 카운터
- RC_Test1 : 0 ( "RC_Test1 deInit" 출력 )
- RC_Test2 : 0 ( "RC_Test1 deInit" 출력 )
- 현재 각 인스턴스의 참조 카운터
Q1) RC_Test2를 참조하는 RC1에만 nil을 할당했는데, 왜 RC_Test2까지 참조 횟수가 감소했을까?
그 이유는 매우 간단하다.
> RC1 인스턴스가 해제되면서 자연스럽게 RC1의 프로퍼티인 A 또한 역할이 끝나게 된다.
> A는 약한 참조로 되어있기 때문에 RC2의 참조 카운트까지 감소되는 것이다.
Q2) print( RC2.B ) 를 했을 시 왜 nil 을 반환하는가?
> RC2 인스턴스의 B는 약한 참조를 하므로 RC1의 인스턴스가 메모리 해제가 되었으니 자동으로 nil 을 할당한다.
3-1. 정리
- Q1
- 어떤 인스턴스가 메모리에서 해제가 된다.
- 그와 동시에 해제된 인스턴스의 프로퍼티가 약한 참조하는 인스턴스의 참조 카운트도 1 감소된다.
- Q2
- 약한 참조를 하는 프로퍼티가 참조하는 인스턴스가 해제가 된다.
- 자동으로 nil을 할당하기 때문에, 항상 꼭! 옵셔널 타입으로 선언해야 한다.
4. Retain Cycle 해결 (2) - Unowned Reference ( 미소유 참조 )
미소유 참조 또한 약한 참조처럼 참조 카운트를 증가시키지 않는 참조 방법이다.
이 둘의 차이 점은 아래와 같다.
- 미소유 참조의 경우 내가 참조하는 인스턴스가 꼭 메모리에 존재할 것이라는 전제가 있어야 한다.
이게 대체 무슨소리야..-_- ㅎㅎ.. 빠르게 코드로 알아보자!
class Person {
let name: String
var car: Car? // (1)
init(name: String) {
self.name = name
print("\(name) being initialized")
}
deinit {
print("\(name) being deinitialized")
}
}
class Car {
let name: String
unowned var owner: Person // (2)
init(name: String, owner: Person) {
self.name = name
self.owner = owner
print("\(name) being initialized")
}
deinit {
print("\(name) being deinitialized")
}
}
var seolhee: Person? = Person(name: "seolhee") // (3)
if let person: Person = seolhee {
person.car = Car(name: "tivoli", owner: person) // (4)
}
seolhee = nil // (5)
- Person은 Car를 꼭 가지지 않아도 된다.
- 강한 참조, 옵셔널로 선언
- Car은 Person(주인)이 꼭 있어야 한다.
- 미소유 참조로 선언
- Person 인스턴스 생성
- 현재 각 인스턴스의 참조 카운터
- seolhee : 1
- 현재 각 인스턴스의 참조 카운터
- Car 인스턴스 생성
- 현재 각 인스턴스의 참조 카운터
- Person ( seolhee ) : 1
- Car : 1
- 현재 각 인스턴스의 참조 카운터
- Person ( seolhee ) 인스턴스 nil 할당
- 현재 각 인스턴스의 참조 카운터
- Person ( seolhee ) : 0
- Car : 0
- 현재 각 인스턴스의 참조 카운터
Q) 그러니까 약한 참조와 미소유 참조의 차이가 정확히 무엇인가요?
> 가장 핵심적인 차이를 모여주는 구문은 (4) 번이다.
> if let 을 통해 Person 인스턴스가 있어야만 Car 인스턴스를 생성 가능하게 구현되어 있다.
( 앞서 말한대로 참조하는 인스턴스는 항상 메모리에 올라와 있는 것을 기준으로 하기 때문!! )
> 그 뒤로는 약한 참조와 마찬가지로 Person 인스턴스를 nil로 할당하였고, Car 인스턴스도 모두 해제가 되었다.
이렇게 미소유 참조를 하게 되면 참조하는 인스턴스의 제약을 쉽게 줄 수 있고
강한 순환 참조 문제도 피해갈 수 있다.
5. 정리
- Retain Cycle
- 각각이 인스턴스가 서로를 강한 참조를 하게 될 경우 발생하는 문제!
- 그로 인해 메모리 누수가 발생하여 성능 저하를 일으키게 된다.
- 약한 참조
- 참조 카운트를 증가시키지 않는다.
- 내가 참조하는 인스턴스가 메모리에서 해제가 되었을 경우 자동으로 nil을 할당해준다.
- 반드시 옵셔널 변수로 선언!
- 미소유 참조
- 참조 카운트를 증가시키지 않는다.
- 내가 참조하는 인스턴스는 반드시 메모리에 존재한다는 가정하에 사용하는 참조
- 옵셔널 변수로 선언하지 않아도 된다!
이상으로 ARC (2) Retain Cycle, 강한참조, 약한참조 그리고 미소유참조 포스팅을 마치겠습니다.
틀린 부분이나 궁금한 사항은 댓글 남겨주세요~
참고
'iOS 🖥️ > Swift' 카테고리의 다른 글
[ Swift ] KVC(Key-Value-Coding), KVO(Key-Value-Observing) (0) | 2023.01.05 |
---|---|
[ Swift ] 서브스크립트 ( Subscript ) (0) | 2022.12.30 |
[ Swift ] ARC (1) 기본 개념, Reference Count 이해 (0) | 2022.12.28 |
[ Swift ] COW ( Copy-On-Write ) (0) | 2022.12.27 |
[ Swift ] Class vs Struct (0) | 2022.12.26 |