[Swift][문법] Closure Capture에 대하여

Uno·2021년 8월 8일
1

Tip-Swift

목록 보기
15/26

클로저에서 공부해야할 개념들이 여러가지가 있습니다만,

그 중에서 좀 까다로울 수 있는 "Capture Values" 부분을 작성하겠습니다.

1. Capture Values???


직역하자면, "값을 잡아둔다" 정도로 해석이 되겠죠.

네, 맞습니다. 값을 잡아둡니다.

A closure can capture constants and variables from the surrounding context in which it’s defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists. - 공식문서 -

해석)

클로저는 상수나 변수를 캡쳐할 수 있습니다. "surrounding context"로 부터요.

그 클로저는 그리고 값을 참조하거나 변경할 수 있습니다. 해당 코드 블럭 내에서요. 심지어 "original scope엔 존재하지 않다고 하더라도요.

정리해보자면, 다음처럼 해석할 수 있겠네요.

특정 context에 해당하는 범위에 값을 캡쳐해둘 수 있고, 값을 참조하거나 변경이 가능하다!

좀 더 설명울 붙여서 정의해볼게요.

(클로저에서) capture values란, "로직 수행을 위해 context를 참조한다."

== Capturing by reference

공식문서의 예제는 다음과 같이 코드를 보여줍니다.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    // 변수를 선언한다.
    var runnungTotal = 0

    // 함수를 선언한다.
    func increment() -> Int {
        runnungTotal += amount
        return runnungTotal
    }

    // 함수를 리턴한다.
    return increment
}

makeIncrementer 메소드를 보면 runningTotal 이라는 변수가 있죠.

그런데 해당 메소드 내부에 보면 increment 라는 메소드가 또 있고, 내부에는 없는 프로퍼티인 runningTotal 값에 접근하고 있습니다.

위에 나오던 영단어 surrounding 이 왜 있는지 느낌이 오시나요?

(해당 메소드를 둘러싸고 있는 context를 캡쳐한다)

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30

이 코드를 보시면 조금 생소하실 수 도 있습니다.

makeIncrementer 메소드는 한 번 호출되면 내부에 있는 메소드의 변수인 runningTotal은 메모리에서 해제되어야 합니다.

그런데 계속 값이 추가되고 있죠?

이 의미는 값이 해제되지 않았다는 뜻입니다.

왜? why?

→ capturing Values...

값을 잡아두고 있기 떄문이죠. 그러면 왜 잡아둘까요?

→ 참조하고 있는 대상이 있기 때문입니다. 참조가 없으면 해제되겠죠.

클로저는 참조타입이므로 값을 공유하게 됩니다.

// 클로저는 참조타입임을 보여주는 예시
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen() // 40
incrementByTen()     // 50

그래서 이전에 추가된 값 30에서 한 번더 호출하게되면, 그 값에 10을 더해 40이 됩니다.

여기서 질문

그러면, value type의 경우는 값이 복사된다고 하는데, 어떻게 값을 참조하지?

이에 대한 대답은 훌륭하신 "kimdo2297" 블로그 링크에서 답변하주시고 있습니다.
https://velog.io/@kimdo2297/클로져-캡쳐에-대해서-about-closure-capture

해당 블로그에서 다음과 같이 답을 주고 있습니다.

  • value type이더라도 reference Count를 생성한다.
  • value type이더라도 클로저가 캡쳐하면 heap 영역으로 이동할 것이다.(혹은 저장될 것이다.)

그러므로,

value Type 도 동일하게 캡쳐될 것이고, 동일하게 동작하게 될 것이다.

로 결론 지을 수 있을 것 같네요.

이후 글은
"https://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/"
을 참조해서 작성했습니다.

2. Capturing Values Example


값이 어떻게 캡쳐되고 저장되는 지에 대한 예제 코드입니다.

먼저 class를 정의합니다.

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
    }
    
    deinit { print("\(self.name) 메모리에서 해제 ") }
}

시간 차이를 두어 값이 어떻게 변화하는지 살피기 위해 DisPatch 메소드를 활용합니다.

func delay(_ seconds: Int, closure: @escaping () -> ()) {
    let time = DispatchTime.now() + .seconds(seconds)
    DispatchQueue.main.asyncAfter(deadline: time) {
        print("타이머 끝")
        closure()
    }
}

본격적으로 테스트를 해보겠습니다.

class를 상수에 할당하고, 1 초뒤에 값을 확인합니다.

func capturingValueTest() {

    let person = Person(name: "Uno")
    print("최초 closure \(person.name)")
    
    delay(1) {
        print("내부 closure \(person.name)")
    }
    
    print("종료")
}

// <Console Result>
// 최초 closure Uno
// 종료
// 타이머 끝
// 내부 closure Uno
// Uno 메모리에서 해제

콘솔을 보면

  1. 최초에 선언될 때, Uno를 호출합니다.
  2. 그리고 메소드가 종료됩니다.
  3. 하지만 아직 메모리에서 해제되진 않았습니다. (reference Count 가 남아있기 때문이죠.)
  4. 그리고 타이머가 종료된 이후,
  5. 내부 print문이 호출됩니다.
  6. 그리고 메모리에서 해제되고 있습니다.

→ 이 예제를 통해서 reference Count가 남아있다면, 메모리에서 해제되지 않음을 알 수 있습니다.

3. 캡쳐된 변수는 실행될 시점에서 값을 가져옵니다. (Captured variables are evaluated on execution)


영어를 직역하니 어떤 의미인지 판단하기 어려워 개인 의견을 조금 더했습니다.

코드를 보겠습니다.

func capturingValueTest02() {
    
    var person = Person(name: "Moya")
    print("최초 closure \(person.name)")
    
    delay(1) {
        print("내부 closure \(person.name)")
    }
    
    person = Person(name: "Swift")
    print("변경이후 closure \(person.name)")
    
}

// <Console Result>
// 최초 closure Moya
// Moya 메모리에서 해제 
// 변경이후 closure Swift
// 타이머 끝
// 내부 closure Swift
// Swift 메모리에서 해제

코드를 보면, 이전과 다른 점이 있습니다.

최초에는 "Moya" 를 멤버변수에 할당했고,

함수의 마지막 부분에서는 "Swift"를 할당했습니다.

그리고 1초가 지난 시점에서 어떤 값이 호출되는지 출력하고 있습니다.

  1. 최초 Moya 입력
  2. Swift 입력
  3. 타이머 종료
  4. print문 출력
  5. 메모리 해제

이 때, 타이머가 종료된 이후에 호출된 closure의 값이 "Swift" 죠.

타이머가 종료된 시점에서 아직 메모리에 class 멤버변수의 값을 참조하고 있었고, 제일 마지막에 변경해준 값을 출력했습니다.

→ 참조를 하는 시점에서의 가장 마지막 값을 출력하게됨을 알 수 있습니다!

그러면 의문이 생깁니다.

그러면, delay가 된 이후의 시점이 아니라 delay가 시작된 시점에서의 값으로 출력할 수는 없나?

4. Capture List를 통해서 메소드가 호출되는 시점에서의 값을 가져온다.


먼저 CaptureList에 대해 짧게 설명드리면 다음과 같습니다.

  • 클로저 내부에서 참조타입을 획득하는 규칙을 정하는 기능입니다.
  • 이 기능을 통해서 메모리 누수의 주 원인인 "강한 참조 순환" 을 막을 수 있습니다.
  • 참조하는 값이 변경되어 클로저 내부의 값이 변경되는 것을 막을 수 있습니다.

코드를 보겠습니다.

func capturingValueTest03() {
    
    var person = Person(name: "Uno")
    print("최초 closure \(person.name)")
    
    delay(1) { [person] in
        print("내부 closure \(person.name)")
    }
    
    person = Person(name: "Moya")
    print("변경이후 closure \(person.name)")
}
capturingValueTest03()

// <Console Result>
// 최초 closure Uno
// 변경이후 closure Moya
// Moya 메모리에서 해제 
// 타이머 끝
// 내부 closure Uno
// Uno 메모리에서 해제

바로 이전 코드와 달라진 점은,

delay 클로저 코드블럭의 값이 클로저를 호출할 때의 값인 "Uno" 라는 점입니다.

참고자료


profile
iOS & Flutter

0개의 댓글