[Swift] 강한 순환 참조와 해결 방법 (weak, unowned)

parkgyurim·2022년 7월 24일
1

Swift

목록 보기
7/8
post-thumbnail

Swift 는 ARC (Automatic Reference Counting) 라는 방식으로 메모리 관리를 하고 있습니다.

개발자가 직접 메모리 할당과 해제를 하지 않고 자동으로 해주기 때문에 편리함도 있지만, ARC 로 부터 발생할 수 있는 문제점 또한 존재합니다.

바로 강한 순환 참조 (Strong Reference Cycle) 인데요,

오늘은 강한 순환 참조가 무엇인지, 그리고 이를 해결할 수 있는 방법이 무엇인지 알아보겠습니다.


💪 Strong Reference Cycle

강한 순환 참조는 여러 인스턴스가 서로 강한 참조를 하는 사이클을 생성하는 것을 의미합니다.

강한 순환 참조가 문제가 되는 이유는 Swift 에서는 ARC 를 이용해서 Reference Count 가 0 이 되면 메모리에서 할당 해제를 하고 있는데, 강한 순환 참조가 생기게 되면 Reference Count 가 0 이 되지 않아 실제로 사용을 하지 않는 인스턴스가 메모리를 차지하고 있는 메모리 누수 현상이 발생하기 때문입니다!

그런데 강한 참조가 무엇일까요?


➡️ References

Swift 에서 Reference Type 의 인스턴스를 참조하는 방식은 3가지가 있습니다.

strong reference

  • 참조하는 인스턴스의 Reference Count 를 증가시킨다.
  • 인스턴스를 참조하는 default 방식 (키워드를 생략하게 되면 strong 참조)

weak reference

  • 참조하는 인스턴스의 Reference Count 를 증가시키지 않는다.
  • 참조하는 인스턴스가 메모리에서 할당 해제될 경우 nil 이 할당된다.

Reference Count 를 증가시키지 않아 강한 순환 참조를 해결할 수 있는 하나의 방법이 될 수 있습니다.

nil이 할당 될 수 있기때문에 옵셔널 타입으로 선언하여야 합니다.

📌 NOTE

ARC 가 weak 변수를 nil로 만들때는 프로퍼티 옵저버 (Property observer) 가 호출되지 않습니다.

unowned reference

  • 참조하는 인스턴스의 Reference Count 를 증가시키지 않는다.
  • 참조하는 인스턴스가 메모리에서 할당 해제될 경우 별다른 조치를 취하지 않는다.

unowned 역시 Reference Count 를 증가시키지 않아 강한 순환 참조를 해결할 수 있는 하나의 방법이 될 수 있습니다.

하지만 weak 와 다르게 옵셔널 타입으로 선언하지 않아도 됩니다.
그 이유는 unowned 참조를 하면 참조하는 인스턴스가 메모리에서 할당 해제되어도 여전히 Heap 영역에 인스턴스가 있던 곳을 가리키고 있습니다. 즉 허상 포인터 (Dangling Pointer) 가 됩니다.

그렇기 때문에, 참조하는 인스턴스가 먼저 메모리에서 해제될 가능성이 없는 경우에만 사용해야합니다!

weak vs unowned

Use a weak reference when the other instance has a shorter lifetime—that is, when the other instance can be deallocated first. In contrast, use an unowned reference when the other instance has the same lifetime or a longer lifetime. - 📚 Swift Documentations - ARC

weakunowned 의 차이점은 참조하는 인스턴스가 메모리에서 할당 해제될 경우 에 볼 수 있습니다.

  • weak 로 선언된 변수는 nil 이 됨
  • unowned 로 선언된 변수는 nil 이 되지 않고 dangling pointer 가 됨

➡️ 그렇기 때문에 Swift 공식문서에서는

  • 참조하는 인스턴스가 더 짧은 lifetime 을 가질때 사용
  • 참조하는 인스턴스가 같거나 더 긴 lifetime 을 가질때 사용

하는 것을 권장하고 있습니다.

"Dangling Pointer", "nil 이 될 수 있다" 의 관점에서 읽어보시면 이해하기 쉽습니다.


🚀 강한 순환 참조 해결 방법

Between Class instances

서로 다른 클래스 인스턴스간에 순환 참조가 있을때, 해당 변수를 weak 또는 unowned 로 선언해서 강한 순환 참조가 생기는 것을 막을 수 있습니다.

주의 사항으로는

  • weak 를 사용하는 경우 옵셔널 타입으로 선언
  • unowned 를 사용하는 경우, 인스턴스간 lifetime을 고려
class A { 
	weak var instance : B?
    
    ...
}

class B {
	weak var instance : A?

	...    
}

Closures

Context Capture

클로저의 특성 중 Context Capture 라는 특성을 알고 계신가요?

Context Capture는 사용할 변수 등 클로저가 실행되는 Context를 캡쳐하는데, 이때 캡쳐는 값을 복사하는 것이 아니라 Reference Capture 를 하게 됩니다. (해당 변수가 Value Type 인지 Reference Type 인지 상관없이 Reference Capture 를 하게 됩니다.)

클로저가 실행되면

  • 캡쳐된 Context 가 Heap 영역에 할당되고,
    이때 Reference Capture 를 했기때문에 클로저는 클래스를 참조하고 있습니다.
  • 클래스는 실행을 위해 Heap 영역에 할당된 클로저를 참조하고 있습니다.

이렇게 클래스 내부에서 클래스 내부를 참조하는 클로저가 실행되면 강한 순환 참조를 일으킬 수 있습니다.

Define Capture List

[weak self] 를 본 적 있으신가요?

이는 self, 즉 (클로저가 속한) 클래스를 약하게 참조하겠다 라는 의미입니다.

클로저의 파라미터와 리턴 타입 앞에 캡쳐 목록을 지정할 수 있고, weak, unowned 키워드로 참조 방식을 지정할 수 있습니다.

// 📌 클로저의 파라미터와 리턴값이 있는 경우
var someClosure1 = { [unowned self, weak delegate = self.delegate] (index: Int, stringToProcess: String) -> String in
	...
}

// 📌 클로저의 파라미터와 리턴값이 없는 경우
var someClosure2 = { [unowned self, weak delegate = self.delegate] in
	...
}

클로저에서도 마찬가지로

  • weak 를 사용할 경우 옵셔널 타입으로 nil을 가질 수 있음을 고려해야함
  • unowned를 사용할 경우 인스턴스간 lifetime을 고려해야함

👍 마무리

오늘은 ARC 에 이어서 강한 순환 참조에 대해서 알아보았습니다.

틀린 정보 또는 궁금한 점이 있다면 댓글 부탁드립니다! 읽어주셔서 감사합니다‼️

0개의 댓글