참고로 포스팅을 시작하기에 앞서 하나 짚고 넙어갈게 있다.
강한 참조와 강한 순환 참조는 다른 말이다.
이에 대해 궁금하면 👇아래 목차 '1. 강한 참조 (strong)'과 '2. 강한 순환 참조 (Strong Reference Cycle)'를 참고하면 된다.
strong vs weak vs unowned 요약된 설명을 보고 싶다면, 목차 '4. strong vs weak vs unowned 요약'을 바로 보면 된다.
'강한 참조(strong)'이란 인스턴스 또는 클로저가 다른 인스턴스 또는 클로저를 단방향으로 참조하는 것을 말한다. 그리고 참조하는 대상의 RC가 +1이 된다.
그렇다면 강한 참조와 강한 순환 참조는 뭐가 다른걸까?
강한 참조 앞에 '순환'이 붙었다. 여기서 유추해볼 수 있다.
'강한 순환 참조(Strong Reference Cycle)'이란 각각의 인스턴스 또는 클로저가 양방향으로 서로 참조하는 것을 말한다.
👇 아래 코드를 봐보자!
(결론) 실행해보면 deinit이 호출어 print문이 출력될 것 같지만, 아무것도 출력되지 않는다.!!
강한 참조를 하고 있기 때문에, Man과 Woman의 인스턴스 각각에 nil 값을 주어도 힙(heap) 메모리에서 제거되지 않는다.
RC의 흐름을 보면 다음과 같이 세 부분으로 쪼개서 볼 수 있다.
1) 인스턴스를 새로 생성
var peter: Man? = Man()
var marie: Woman? = Woman()
인스턴스를 새로 생성하면서 메모리(heap)에 생성된 Man과 Woman의 인스턴스를 가리키는 RC = 1로 초기화된다.
2) 각각의 인스턴스가 순환 참조하도록 설정
peter?.wife = marie
marie?.husband = peter
각각의 인스턴스가 순환 참조되면서 기존 RC에 +1되어 RC = 2 로 설정된다.
3) 각각의 인스턴스를 nil로 초기화
peter = nil
marie = nil
힙 영역을 가리키는 인스턴스 변수가 nil로 설정되면서 RC-1이 된다.
따라서 RC = 2에서 RC = 1로 바뀌어 메모리(heap)에서 제거되지 않는다. 메모리에서 제거되지 않기 때문에 소멸자 deinit( )도 호출되지 않았고, print( )문도 출력되지 않았다.
강한 순환 참조를 하면 2번 목차의 예시에서 보았듯이, 메모리 누수(memory leak)가 발생할 가능성이 있다.
'메모리 누수(memory leak)'란 강한 순환 참조에 의해 메모리의 힙 영역에서 해제되지 않고 계속 남아있는 현상을 말한다. 따라서 메모리 낭비, 누수(leak)가 발생하는 것이다.
메모리 누수 현상이 지속되면 메모리(Heap) 공간이 꽉 차게 되어 프로그램이 다운되는 현상이 발생할 수도 있다. 따라서 메모리 누수를 방지하는 코드를 작성하는게 중요하다. 관련해서는 목차 '3. 메모리 누수를 위한 해결책'에서 자세히 살펴보자.
'약한 참조'는 참조시 참조하는 대상의 RC가 +1이 되지 않는다.
참조하고 있는 메모리가 해제될 경우, 자동으로 nil로 초기화되어 강한 참조를 풀어준다. (RC-1)
nil이 할당될 수 있다는 측면에서 반드시 weak를 선언한 변수는 optional type이어야 한다.
따라서 접근시 안전하게 접근하려면 옵셔널 바인딩(if let, guard let) 혹은 옵셔널 체이닝을 통해 접근하는 것이 좋다.
weak 선언 기준? 수명이 더 짧은 인스턴스를 가리키는 인스턴스를 약한 참조로 선언하면 된다!
ex) Peter가 먼저 죽는다. -> Marie의 husband가 nil이 될 수 있다. -> Marie의 husband를 weak로 선언
자세한 설명은 아래 코드를 보면서 확인해보자!
'2. 강한 순환 참조(Strong Reference Cycle)' 목차에서 보았던 코드와 다른 점은 아래와 같이 순환 참조시 weak 키워드를 추가해주었다.
weak var husband: Man?
(결론) 실행해보면 순환 참조를 하되, 약하게(weak) 순환 참조하여 각
인스턴스의 소멸자 deinit이 호출되었다.
또한, 주목할 점은 Man의 인스턴스 peter가 heap 메모리에서 제거되면서, peter를 참조하고 있던 Woman의 인스턴스인 marie의 husband 프로퍼티도 동시에 nil로 초기화 되었다.
'2. 강한 순환 참조(Strong Reference Cycle)' 목차에서는 nil로 초기화해도, 소멸자가 호출되지 않았는데 어떻게 호출된걸까?
RC의 변화되는 흐름을 살펴보자!
1) 인스턴스를 새로 생성
var peter: Man? = Man()
var marie: Woman? = Woman()
인스턴스를 새로 생성하면서 메모리(heap)에 생성된 Man과 Woman의 인스턴스를 가리키는 RC = 1로 초기화된다. (강한순환참조와 동일)
2) 각각의 인스턴스가 순환 참조하도록 설정
peter?.wife = marie
marie?.husband = peter
강한 순환 참조는 각각의 인스턴스가 순환 참조되면서 기존 RC에 +1되어 RC = 2 로 설정된다.
하지만, weak로 선언되면 순환참조되어도 참조하고 Reference의 Count에 영향이 없다.
따라서 Man과 Woman 인스턴스 RC = 1로 그대로다.
3) 각각의 인스턴스를 nil로 초기화
peter = nil
marie = nil
힙 영역을 가리키는 인스턴스 변수가 nil로 설정되면서 RC-1이 된다.
따라서 RC = 1에서 RC = 0으로 바뀌어 메모리(heap)에서 제거되면서, 소멸자 deinit( )도 호출된다.
최종적으로 print( )문도 출력된다.
'무소유 참조(unowned)'는 약한 참조(weak)와 마찬가지로 참조시 참조하는 대상의 RC(Reference Count)가 +1이 되지 않는다.
(weak와 unowned의 차이점) weak는 객체를 계속 추적하다가 메모리(heap)에서 제거되며 nil로 바뀐다. 하지만, unowned는 추적하던 객체가 메모리에서 제거되면 *댕글링 포인터가 남는다.
*'댕글링 포인터(Dangling Pointer)'란? 참조하고 있던 객체가 메모리에서 제거되면서 할당되지 않는 허공을 바라보고 매달려(dangling) 있는 포인터
🚨 주의할 사항!
무소유로 선언된 변수가 가리키던 인스턴스가 메모리에서 먼저 해제된 후, 접근하려 하면 런타임 에러가 발생한다.
자세한 설명은 아래 예제를 봐보자!
👇 unowned로 선언된 인스턴스가 먼저 힙에서 제거되었을 때, 어떻게 에러가 발생하는지 코드를 작성해보았다.
unowned var wife: Woman?
marie의 수명이 더 길꺼라고 가정하고, Woman의 인스턴스에 unowned 무소유 참조를 선언하였다.
marie = nil
그리고 marie에 nil값을 주어 heap에서 제거하였다.
결과를 확인해보면 Woman이 메모리에서 해제되면서 deinit의 print()문이 출력되었다. 그리고 다음 줄에 marie의 프로퍼티인 husband는 메모리에서 제거되면서 nil로 초기화됬음을 확인할 수 있다.
error: Execution was interrupted, reason: signal SIGABRT
이때 아직 힙에서 제거되지 않은 Man 인스턴스에서 메모리에서 제거된 wife에 접근하면 아래와 같은 에러가 발생한다. 위에서 설명한 허공을 참조하는 '댕글링 포인털'를 참조하게 되어 crash가 발생한 것이다.
(결론!!) 따라서 unowned는 사라지지 않을거라고 보장되는 객체에만 설정해야 한다.
strong, weak, unowned에 대해서 주저리 주저리 길게 적어보았는데...ㅋㅋ
🤔 그래서 strong, weak, unowned의 차이점은 뭐고.. 각각 언제 사용하면 될까?
👇 그래서 아래 표로 요약해보았다.
요약 | strong | weak | unowned |
---|---|---|---|
RC | RC + 1 | 영향 x | 영향 x |
특징 | 강한 순환참조 발생 가능성 존재 | 추적하던 인스턴스가 메모리에서 삭제되면 자동으로 nil 할당 | 추적하던 인스턴스가 메모리에서 먼저 삭제되면 댕글링 포인터가 남음 |
단점 | 강한 순환참조 발생시 memory leak 발생 | 옵셔널 바인딩을 통해 접근해야 하기 때문에 코드가 길어짐 | 댕글링 포인터에 접근하면 crash 발생 |
사용 기준 | 1. 키워드 선언 안 하면 default로 strong 2. RC를 증가시켜 ARC로 인한 메모리 해제를 피하고 싶을 경우 | 1. 강한 순환 참조로 인한 메모리 릭을 방지하고 싶을 때 2. delegate 패턴에서 사용 | 1. 개발자가 인스턴스의 라이프 사이클을 확실히 알고 있어, 추적하는 인스턴스가 먼저 메모리에서 삭제되지 않을 경우 2. 강한 순환 참조로 인한 메모리 릭을 방지하고 싶고, 언래핑 과정 없이 간결한 코딩을 원할 때 |
타입 | 상관 없음 | 항상 옵셔널 타입으로 선언 | 상관 없음 |