[Swift 기초] - strong, weak, unowned, 강한 순환 참조

justdotheg·2024년 1월 18일
0
post-thumbnail

참고로 포스팅을 시작하기에 앞서 하나 짚고 넙어갈게 있다.
강한 참조와 강한 순환 참조는 다른 말이다.
이에 대해 궁금하면 👇아래 목차 '1. 강한 참조 (strong)'과 '2. 강한 순환 참조 (Strong Reference Cycle)'를 참고하면 된다.

strong vs weak vs unowned 요약된 설명을 보고 싶다면, 목차 '4. strong vs weak vs unowned 요약'을 바로 보면 된다.


1. 강한 참조 (strong)

'강한 참조(strong)'이란 인스턴스 또는 클로저가 다른 인스턴스 또는 클로저를 단방향으로 참조하는 것을 말한다. 그리고 참조하는 대상의 RC가 +1이 된다.

  • 선언할 때 아무것도 적어주지 않으면, 모든 인스턴스 또는 클로저는 default로 강한 참조(strong) 하게 되어 있다.
  • 그리고 단방향으로 대상을 참조할 때, 무조건 대상(인스턴스 또는 클로저)의 RC(Reference Count)가 +1이 된다.

2. 강한 순환 참조 (Strong Reference Cycle)

그렇다면 강한 참조와 강한 순환 참조는 뭐가 다른걸까?
강한 참조 앞에 '순환'이 붙었다. 여기서 유추해볼 수 있다.

'강한 순환 참조(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( )문도 출력되지 않았다.

3. 강한 순환 참조의 문제점 - 메모리 누수(memory leak)

강한 순환 참조를 하면 2번 목차의 예시에서 보았듯이, 메모리 누수(memory leak)가 발생할 가능성이 있다.

'메모리 누수(memory leak)'란 강한 순환 참조에 의해 메모리의 힙 영역에서 해제되지 않고 계속 남아있는 현상을 말한다. 따라서 메모리 낭비, 누수(leak)가 발생하는 것이다.

메모리 누수 현상이 지속되면 메모리(Heap) 공간이 꽉 차게 되어 프로그램이 다운되는 현상이 발생할 수도 있다. 따라서 메모리 누수를 방지하는 코드를 작성하는게 중요하다. 관련해서는 목차 '3. 메모리 누수를 위한 해결책'에서 자세히 살펴보자.

3. 메모리 누수를 위한 해결책

3.1. 약한 참조 (weak)

'약한 참조'는 참조시 참조하는 대상의 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( )문도 출력된다.

3.2. 무소유 참조 (unowned)

'무소유 참조(unowned)'는 약한 참조(weak)와 마찬가지로 참조시 참조하는 대상의 RC(Reference Count)가 +1이 되지 않는다.

3.2.1. weak와 unowned 차이점이 뭘까?

(weak와 unowned의 차이점) weak는 객체를 계속 추적하다가 메모리(heap)에서 제거되며 nil로 바뀐다. 하지만, unowned는 추적하던 객체가 메모리에서 제거되면 *댕글링 포인터가 남는다.

*'댕글링 포인터(Dangling Pointer)'란? 참조하고 있던 객체가 메모리에서 제거되면서 할당되지 않는 허공을 바라보고 매달려(dangling) 있는 포인터

3.2.2. unowned 사용시 주의사항

🚨 주의할 사항!
무소유로 선언된 변수가 가리키던 인스턴스가 메모리에서 먼저 해제된 후, 접근하려 하면 런타임 에러가 발생한다.

자세한 설명은 아래 예제를 봐보자!

👇 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는 사라지지 않을거라고 보장되는 객체에만 설정해야 한다.

4. strong vs weak vs unowned 요약

strong, weak, unowned에 대해서 주저리 주저리 길게 적어보았는데...ㅋㅋ

🤔 그래서 strong, weak, unowned의 차이점은 뭐고.. 각각 언제 사용하면 될까?

👇 그래서 아래 표로 요약해보았다.

요약strongweakunowned
RCRC + 1영향 x영향 x
특징강한 순환참조 발생 가능성 존재추적하던 인스턴스가 메모리에서 삭제되면 자동으로 nil 할당추적하던 인스턴스가 메모리에서 먼저 삭제되면 댕글링 포인터가 남음
단점강한 순환참조 발생시 memory leak 발생옵셔널 바인딩을 통해 접근해야 하기 때문에 코드가 길어짐댕글링 포인터에 접근하면 crash 발생
사용 기준1. 키워드 선언 안 하면 default로 strong

2. RC를 증가시켜 ARC로 인한 메모리 해제를 피하고 싶을 경우
1. 강한 순환 참조로 인한 메모리 릭을 방지하고 싶을 때

2. delegate 패턴에서 사용
1. 개발자가 인스턴스의 라이프 사이클을 확실히 알고 있어, 추적하는 인스턴스가 먼저 메모리에서 삭제되지 않을 경우

2. 강한 순환 참조로 인한 메모리 릭을 방지하고 싶고, 언래핑 과정 없이 간결한 코딩을 원할 때
타입상관 없음항상 옵셔널 타입으로 선언상관 없음

0개의 댓글