본문 바로가기
Dev/Swift 내 정리

[Swift] Closure Capture list, ARC, AnyObject까지 연결지어서 정리

by Mintta 2023. 4. 23.
키워드 공부 과제에서 Closure Capture list, ARC, AnyObject에 대해서 알아보는 것이였는데 이를 연결지어서 정리해보았습니다

Closure Capture list

var str = "Hello, World!"
var myClosure = { [str] in
    print (str)
}
str = "next"
let inc = myClosure
inc()
  • 이 코드를 실행시키면 어떻게 될까요?
  • “Hello, World!”가 출력됩니다
var str = "Hello, World!"
var myClosure = {
    print (str)
}
str = "next"
let inc = myClosure
inc()
  • 이 코드를 실행시키면 어떻게 될까요?
  • “next”가 출력됩니다.

이런 일이 벌어지는 이유가 뭘까요❓

바로 capture list때문입니다.

첫번째 코드에서는 str가 외부 변수를 capture해서 쓰기 때문에 해당 클로저를 실행시키면 “Hello, World!”가 출력 됩니다. 하지만 두번째 코드에서는 capture list가 없기 때문에 원래 closure대로 동작합니다.

Swift의 클로저는 실행중에 필요한 타입들을 capture합니다. 값들에 대한 immutable한 copy를 만들어내는거죠.

값을 캡쳐해서 immutable한 copy를 만들어내는 것은 reference type이 아닌 value type에 적용됩니다. 캡처 목록을 사용하는 경우에도 reference type의 경우 reference semantics로 인해 모두 동일한 객체를 참조하게 됩니다.

 

왜? value type은 stack에 저장되고

(또 항상은 아님ㅎ)

, reference type은 stack에 reference가 저장되고 heap에 내용이 저장되기 때문에 !


default로 Swift는 strong reference로 capture를 합니다. 이 말은 즉, 클로저가 클로저 안에 있는 값들에 대해 strong reference를 만든다는 것입니다.

  • Reference count가 1증가한다.
  • ARC는 객체를 메모리에서 deallocate시킬 때 Reference count가 0이냐 아니냐를 기준으로 할당해제합니다.
  • 0이라면 “아 해제시켜도 안전하겠구나” 하고 deallocate시키는 반면에 0보다 높다면 “누가 쓰고있구나”고 생각하고 넘어갑니다.
dataManagerClass.articlesDidChange = {result in
  self.articles = result.0
  self.metaData = result.1
}
  • ViewController에서 dataManagerClass객체가 있고 해당 객체에 클로저 프로퍼티가 있고 해당 코드는 ViewController에 있는 코드입니다.
  • 그리고 VC에서 일어나는 network 통신이라고 생각하고 봅시다.
  • Network 통신은 보통 굉장히 오래걸리는 작업으로 여기고 비동기로 처리하죠.
  • 여기서 네트워크 통신 결과는 ViewController가 release된 후에 결과가 올 수도 있습니다.

그런데 ViewController가 release될 수 있을까요..?

 

앞서 살펴봤듯이 closure는 closure내부에서 사용되는 값에 대해 capture를 합니다.

dataManagerClass는 클로저 타입의 프로퍼티인 articlesDidChange를 강한 참조로 가지고 있고 articlesDidChange closure는 내부에서 self를 사용하기 때문에 이를 capture합니다. 해당 코드에서 self는 ViewController를 말하겠죠.

따라서 ViewController 클래스의 reference count는 1증가합니다.

그리고 ViewController는 default로 dataManagerClass를 강한 참조로 가지고 있습니다. 그럼 아래 그림과 같은 상황이 됩니다.

Retain Cycle

 

서로가 서로를 강한 참조로 붙잡고 있어 저 둘의 reference count는 1에서 절대 줄어들지 않습니다. (이것을 우리는 Retain Cycle이라고 부릅니다)

ARC가 객체를 메모리에서 할당해제할 때 할당 해제해도 될지 안될지에 대한 기준이 reference count가 0일 때라고 말했었죠??

즉, 1에서 더이상 줄어들지 않는다는 것은 메모리에서 release될 수 없다는 것을 말합니다.

여기서 우리는 weakunowned키워드를 사용해서 retain cycle을 깰 수가 있습니다.

weakunowned 키워드를 사용하면 reference count를 증가시키지 않습니다.

dataManagerClass.articlesDidChange = { [weak self] result in
    guard let self = self else {return}
    self.articles = result.0
    self.metaData = result.1
}
  • 위와 같이 코드를 바꿔준다면 dataManagerClass의 클로저는 ViewController를 약한 참조로 가지고 있게 됩니다.

Break Retain Cycle

  • 그럼 이제 위와 같은 그림의 형태가 되게됩니다.
  • weak는 reference count를 증가시키지 않기 때문에 ViewController의 refernce count는 ViewController의 참조가 끝나는 순간 0이 될테고, ViewController가 할당 해제된다면 자연스럽게 dataManagerClass도 해제되면서 메모리 누수가 없어지게 됩니다.

여기까지 봤다면 ARC가 대충 어떤 존재고 왜 존재하는지 대충 감이 오지 않나요?

ARC

ARC(Auto Reference Counting)는 자동으로 메모리 관리를 하는데 사용됩니다.

객체에 대한 reference count를 관리하고 0이 되면 자동으로 메모리에서 할당해제시켜줍니다. 이런 작업들은 compile time에 이루어집니다. (이 부분은 WWDC - ARC에서 정리)

 

왜 존재하냐구요? ARC가 없다면 저희(개발자)가 직접 객체의 생성과 소멸을 추적하고 참조 관계를 파악해서 관리해주어야합니다….벌써 복잡하지 않나여 (근데 옵젝씨 시절에는 MRC(Manual Reference Counting)이라서 수동으로 했었다네요..)


마지막 질문 다음 중 ARC와 관련이 없는 것을 모두 고르시오.

  • stack
  • enum
  • class

 

 

정답: stack, enum

AnyObject

모든 클래스가 암묵적으로 준수하는 프로토콜입니다.

AnyObject can be used as the concrete type for an instance of any class, class type, or class-only protocol.

AnyObject는 클래스, 클래스 타입 또는 클래스 전용 프로토콜의 인스턴스에 대한 구체적인 타입으로 사용될 수 있습니다.

일단 여기까지 인지한 채로 넘어가볼게요.

 

버튼을 눌러서 뒤로 가면 버튼 누른 횟수가 뜨는 코드 예시를 가지고 와봤습니다.

/// VC

let pressCountViewController = PressCountViewController()
pressCountViewController.delegate = self
self.present(pressCountViewController, animated: true)
protocol PressCountViewControllerDelegate: AnyObject {
    func didTapBackButton(count: Int)
}

final class PressCountViewController: UIViewController {

    private var count = 0

    weak var delegate: PressCountViewControllerDelegate?

        ...
}
  • weak을 붙여주는 이유는 위에서 살펴본 retain cycle 때문에 !
  • 여기는 왜 retain cycle일까???

우선 delegate앞에 weak가 없다고 생각해볼게요.

그렇다면 위의 그림과 같은 상황이 나옵니다. 위에서 우리가 살펴봤던 상황이랑 비슷하죠??

여기서도 weak를 통해서 간단히 해결이 가능합니다. 이제 위에 적힌 코드대로 weak가 있다면 아래 그림처럼 되겠죠

 

자 그러면 weak를 써서 retain cycle을 해결해보았습니다 이제 다시 돌아가봅시다 !

 

  • 기억을 더듬어보면 weak를 쓰고 AnyObject를 안썼다면 어떻게 됐는지 기억하시나요?

에러 메시지를 가지고 왔습니다.

 

'weak' must not be applied to non-class-bound 'any PressCountViewControllerDelegate';
consider adding a protocol conformance that has a class bound

해석해볼까요?

 

클래스에 바인딩되지 않은 임의의 ~ViewControllerDelegate에는 weak를 적용할 수 없다. 클래스 바인딩을 가진 protocol conformance를 추가할 것을 고려해라.

 

AnyObject를 안쓰면 왜 이런 에러를 내뱉는걸까요?

 

우리가 weak를 쓰는 이유가 뭐였죠?

retain cycle을 막기 위해서.

retain cycle은 뭐였죠?

객체가 서로를 강한 참조로 붙잡고 있어서 메모리에서 할당 해제되지 않고 메모리에 계속 떠있는것 (메모리 누수)

 

결국 우리는 reference에 관련된 얘기를 계속 하고 있습니다.

하지만 protocol은 struct, enum, class등 모두가 채택하고 사용할 수 있습니다.

위의 질문에서 모두들 아셨겠지만 struct와 enum이 참조와 상관이 있었나요? 상관이 없었죠.

 

그렇기 때문에 AnyObject를 쓰지 않는다면 구조체,열거형, 클래스 등에 모두 채택가능한 프로토콜이 어디에 채택될지 컴파일러는 모를 수 밖에 없습니다.

AnyObject는 앞에서 살펴봤듯이 클래스, 클래스 타입 또는 클래스 전용 프로토콜의 인스턴스에 대한 구체적인 타입으로 사용될 수 있습니다.

우리는 그렇기 때문에 AnyObject를 붙임으로써 해당 protocol은 클래스 전용이야 ! 라고 알려주는 거죠 !

No error

 

 

Ref

https://medium.com/swlh/using-capture-lists-in-swift-19f408f986d

https://zeddios.tistory.com/213

https://sujinnaljin.medium.com/ios-arc-%EB%BF%8C%EC%8B%9C%EA%B8%B0-9b3e5dc23814

댓글