closure에서는 왜 self를 사용해야 할까?

closure에서는 왜 self를 사용해야 할까?

class에서 closure를 사용할 때 closure 안에서 객체의 변수 또는 함수에 접근할 때 self를 붙여서 사용을 해야합니다. class안에서는 self없이 변수 및 함수를 사용할 수 있고 로컬변수도 바로 사용할 수 있는데 왜 closure에서는 self를 붙여야 할가 궁금했습니다.

objective-c에서도블럭안에서 self를 명시적으로 사용해야합니다. 물론, self없이 사용은 가능하지만 warning 메시지가 발생합니다.

이 부분을 이해하기 위해서는 먼저 capture list에 대해서 알아야합니다. capure list는 closure 안에서 사용할 객체를 정의하는 목록입니다. 문법 구조는 아래 와 같습니다.

let closure: (Bool)->(T) = { [capture list 작성 ] (parameter) in
// closure body
}

capture list는 여러개 정의가 가능하고 콤마(,)로 구분합니다. 정의한 항목이 class인 경우 ‘weak’ 또는 'unowned’를 함께 사용할 수 있습니다.

예를 들면, 아래 코드처럼 작성할 수 있습니다.

func testCaptureList() {
    let aString1: String = "capture1"
    let aString2: String = "capture2"
    
    let closure: (Bool) -> () = { [capture1 = aString1, capture2 = aString2] (success: Bool) in
        print(capture1, capture2)
    }
}

aString1은 capture1에 할당하고 aString2은 capture2에 할당했습니다.
capture1과 capture2는 closure 안에서만 사용가능합니다.

capture list를 사용하는 이유

capture list를 사용하는 이유는 순환 참조 문제와 원하는 객체 또는 값만 복사해서 사용할 때 입니다.

  1. 순환 참조 문제
    closure에서 객체를 capture할 때 순환 참조가 발생할 수 있습니다. closure를 소유한 객체를 closure에서 다시 참조를 한다면 순환 참조가 발생합니다.
    순환 참조가 발생하는 코드를 보겠습니다.

    class TestRetainCycle {
        var property: String = ""
        var comletionHandler: ()->() = {}
    
        func occurRetainCycle() {
            comletionHandler = {
                print(self.property)
            }
        }
    }
    

    TestRetainCycle class에서 completionHandler를 소유하고 있으며 occurRetainCycle메소드를 호출하면 completionHandler가 TestRetainCycle객체를 capture하기 때문에 순환 참조가 발생합니다.
    메모리 참조 관계를 그림으로 표현하면 아래와 같습니다.
    enter image description here
    다음처럼 self를 weak으로 참조하도록 변경하면 순환 참조 문제는 해결이 됩니다.

    class TestRetainCycle {
        var property: String = ""
        var comletionHandler: ()->() = {}
    
        func occureRetainCycle() {
            comletionHandler = { [weak self] in
                print(self?.property as Any)
            }
        }
    }
    

    위 코드의 메모리 참조 관계는 아래와 같습니다.
    enter image description here
    위 사례 처럼 순환 참조 문제 해결을 위해 capture list를 사용할 수 있습니다.

  2. 객체복사
    closure를 생성한 시점에 값을 실제 closure를 호출할 때도 유지하고 싶을 때 사용할 수 있습니다. 아래처럼 코드로 설명드리겠습니다.
    – 첫번째 코드

    func testCaptureList1() {
        var count = Int(0)
        
        let closure = {
            print("closure:",  count))
        }
        count = 10
        
        closure()
        print("count:", count)
       }
       // 결과
       // closure: 10
       // count: 10
    

    – 두번째 코드

    func testCaptureList2() {
        var count = Int(0)
        
        let closure = { [captureCount = count] in
          print("closure:", captureCount)
        }
        count = 10
            
        closure()
        print("count:", count)
    }
    // 결과
    // closure: 0
    // count: 10
    

    첫번째 코드는 capture list에 정의 없이 count를 바로 사용하고 있으며 생성 후에 count값이 변경되면 closure가 호출될 때도 변경된 count값이 적용됩니다. 두번째 코드는 capture list에 count를 captureCount로 정의하였고 count가 변경된 후에 closure가 호출되어도 생성된 시점의 count값이 적용됩니다. 즉, closure가 생성되는 시점의 값이 중요할 때 cature list를 이용할 수 있습니다.


다시 self 이야기로 돌아가겠습니다. class안에서 closure를 생성하는 상황을 가정하겠습니다.
capture list에는 자신이 capture하고자 하는 객체를 명시적으로 정의할 수 있습니다. 또한, 암묵적으로 self가 capture list에 자동으로 추가됩니다.
따라서 closure에서는 instance 객체에 바로 접근할 수가 없으며 capture list에 자동으로 추가된 self 변수를 통해서 접근해야합니다. 그렇기 때문에 closure 안에서 self 접근자 없이 객체의 property 또는 메소드에 접근할 수 없었던 거였습니다.
class자체의 self 접근자가 아니라 capture list에 있는 self 변수에 접근한다고 하면 closure 안에서 self 꼭 써야하는 것을 이해할 수 있습니다.


참고: https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID544
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID56

댓글

이 블로그의 인기 게시물

dismiss에 대해서 알아봅시다

1. Framework: framework란?

2. Framework: 이름충돌