Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

도니의 iOS 프로그래밍 세상

[Swift] ARC와 클로저, 클로저 memory leak - ARC 기초편 4탄 본문

Swift

[Swift] ARC와 클로저, 클로저 memory leak - ARC 기초편 4탄

Donee 2023. 1. 11. 19:00

안녕하세요, 지난 시간에는 ARC의 정의, Strong Reference Cycle, weak unowned의 차이에 대해서 공부했습니다.

이번 시간에는 weak unowned의 예시로 들었었던 View Controller의 클로저 내에서 weak self를 해주는 이유를 바로 알고는 싶지만, 그 전에 closure, closure와의 순환 참조를 통해서 ARC 기초를 마무리하도록 하겠습니다!

클로저는 Reference type

클로저는 reference type이라는 걸 알고 계셨나요?

이전 포스팅에서도 말했듯, reference type의 경우 “참조”가 가능하고, reference count를 증가시키는 것 또한 가능합니다.

그렇다면, closure와 “누군가”도 서로 참조를 일으키는 “순환 참조”문제를 일으킬 수 있는건가요?

네 맞습니다! closure는 reference type이고 이를 누군가가 가르키고 있고 closure또한 상대방을 가르키고 있다면 순환참조 문제를 충분히 발생시킬수 있습니다.

다음 예시를 보도록 하겠습니다

class Person {
    let name: String
    
    lazy var introduction: () -> String = {
        return "안녕하세요 저는 \\(self.name)입니다"
    }
    
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\\(name) is being deinitialized")
    }
}

var rock: Person? = Person(name: "rock")
print(rock?.introduction())
rock = nil

introduction은 lazy property로서 클로저를 가지고 있습니다. (lazy를 사용하는 이유는 self 내부 값을 참조하기 때문에 self가 확정나고 사용한다는 의미로 생각하면 좋습니다. 자세한 설명은 또다른 포스팅에서 다루도록 하겠습니다!)

해당 클로저는 name의 값을 가져와서, 원하는 출력값으로 만들어줍니다.

위의 출력 결과는 어떻게 될까요? instance가 정상적으로 해제가 잘 되었을까요?

Optional("안녕하세요 저는 rock입니다")

deinit이 호출되지 않았습니다. 따라서 해당 값은 정상적으로 메모리에서 해제가 되지 못한거죠!

왜 이렇게 되었는지 그림을 통해 알아보겠습니다

위 그림과 같이, Person객체를 변수 “rock”과 클로저가 참조하고 있습니다.

rock을 해제한다고 하더라도, 여전히 reference count는 1이겠죠?

그리고, Person instance와 클로저는 서로를 참조하여 cycle이 발생하고 있습니다.

이럴경우 절대로! 메모리에서 해제가 되는 건 불가능합니다.

 

그렇다면 우리는 이를 어떻게 해결할 수 있을까요?

reference count를 증가시키지 않도록!, cycle을 발생시키지 않도록!, weak와 unowned를 사용해서 참조를 해주면 되겠네요! 라고 생각하시면 좋습니다~! 

Capturing List

weak, unowned를 어디에 사용해야 할까요? 그전처럼 closure앞에도 weak를 붙이면 될까요?

weak lazy var introduction: () -> String {
...
}

위와 같은 방식으로 해결하려고 하면 컴파일러에서 “에러”가 발생합니다

weak는 class나 class-bound protocol에서만 사용이 가능하기 때문이죠.

Swift는 이를 해결하기 위해 클로저 캡처 목록이라는 해결책을 제공합니다.

클로저의 캡처 방식에 대해서는 상세 포스팅을 따로 하도록 하고 속성으로 알려드리도록 하겠습니다!

결론부터 먼저 말씀드리자면 클로저는, 클로저내에 값들을 reference으로 capture해서 갖고 있습니다.(global function의 경우

이게 무엇일까요?

예시를 들면 더욱 쉽게 이해할 수 있습니다.

func closureCaptureTestDefault() {
    var num = 30
    let printNum = { 
        print(num)
    }
    num = 40
    printNum()
}

위의 결과값은 무엇일까요? 40입니다. 이전에 프로그래밍을 했던 분들이라면 의례적으로 사용했던 패턴입니다.

근데 여기서 살짝 변화를 줘보도록 하겠습니다.

func closureCaptureTest() {
    var num = 30
    let printNum = { [num] in
        print(num)
    }
    num = 40
    printNum()
}

정답은 30입니다.

전, 후가 값이 다른 이유는 첫번째의 경우 value type임에도 reference를 “캡처”한 것이고, 두번째의 경우 capturing list를 통해서 값을 “복사”해서 캡처했기 때문입니다.

capturing 리스트에 대해서 조금 더 알아보겠습니다.

Capturing List

캡처목록의 정의는 애플 문서에서 다음과 같이 정의합니다.

캡처 목록은 클로저 바디내에서 하나 이상 reference type을 캡처할 때 사용할 규칙입니다.

capturing list의 사용 방법은 클로저 시작때 [ ] in을 작성합니다. 그리고 [ ] 안에 캡쳐할 대상 멤버들을 작성하면 됩니다.!

let closure = { [member1] in 
}

let closure = { [member1, member2] in 
}

클로저는, 클로저내에 값들을 reference로 갖고 있습니다.

하지만 “Value Type”의 경우에도 reference로 가지고 있지요.

이 Value Type을 기존과 같이 복사해서 가져오고 싶다면 capturing list를 활용하는 게 가능합니다.

그렇다면 Reference Type은 어떻게 활용할 수 있을까요?

Capturing List & Reference Type

결과부터 말씀드리자면, Capturing list에 weak, unowned를 통해서 Reference Type을 가져오면 됩니다!

let closure = { [weak referenceVariable1, weak referenceVariable2] in 
}

이러한 방식으로요!

이렇게 참조해서 가져온 변수들과 클로저간에는 순환 참조가 발생할까요?

정답은! “아니요”입니다. weak를 통해서 변수를 가져왔기 때문에, reference count를 증가시키지 않습니다~!

이를 아까 deinitialization이 정상적으로 되지 않았던 예시에 적용해봅시다.

class Person {
    let name: String
    
    lazy var introduction: () -> String = { [weak self] in
        return "안녕하세요 저는 \\(self?.name)입니다"
    }
    
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\\(name) is being deinitialized")
    }
}

var rock: Person? = Person(name: "rock")
print(rock?.introduction())
rock = nil

캡처 list에 self라는 인스턴스를 캡처해주었습니다. 이때 self instance 캡처시 “weak”로 지정했기 때문에 서로간의 순환참조는 발생하지 않겠네요!

그러면, 결과는 어떻게 될까요?

Optional("안녕하세요 저는 Optional(\\"rock\\")입니다")
rock is being deinitialized

그림으로 표현하자면 다음과 같습니다.

그전과 다르게, Weak를 통해서 참조하였기 때문에, cycle이 발생하지 않았습니다.

이전 포스팅에서 말했듯이, unowned로도 바꾸어서 실행하셔도 정상 작동됩니다.

unowned를 통해서는 nil-coalescing, optional등이 생성되지 않아 조금 더 편하게 사용이 가능합니다.

하지만 앱 크래쉬가 날 수 있기 때문에 항상 조심해야겠죠

 

이제 클로저 내에서 어떨때 cycle이 발생하는지 정확히 알게 되었습니다.

다음 시간에는, ARC 응용편 5탄, ViewController에서 클로저와 어떨때 lazy deinitialization, memory leak이 발생할 수 있을지 알아봅시다!(드디어 응용편이네요!!)

결론

  1. Closure는 reference type이다
  2. Closure와 순환 참조가 발생할 수 있다.
  3. 순환참조를 해결하기 위해서 capturing list를 활용한다.
  4. capturing list을 활용하면 closure에서도 외부 value type값을 복사하여 사용이 가능하다.
  5. capturing list를 통해서 reference type참조시 weak, unowned로 지정하여 cycle을 방지한다.

참고

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID56

https://docs.swift.org/swift-book/LanguageGuide/Closures.html

Comments