본문 바로가기
iOS/Swift

Swift 클로저(2/3) - 클로저의 캡처

 

 

 

 

 

안녕하세요, UX를 고려하는 개발자 유자🍋입니다.

 

오늘은 클로저의 캡처란 무엇인지에 관해서 공부해 보겠습니다.

 

 

 

 

1. 캡처값 (Capturing Values)

 

 

클로저

 

 

 

값을 캡처한다는 것이 무엇인지

클로저 형태 중 하나인 중첩 함수 예시를 통해 알아보겠습니다.

 

 

 

func outer(base: Int) -> () -> Int {
    var num = 20 + base
    func inner() -> Int {
        num += 1
        return num
    }
    return inner
}

let result = outer(base: 5)
result()

 

 

 

 

위 예시 코드를 보면


outer 함수는 내부 함수 inner를 반환하는 형식으로 정의되어 있으며,
내부 함수 inner는 외부함수에서 정의된 변수 num에 1을 더한 값을 반환합니다.

 

 

이때 내부 함수인 inner 함수만 따로 보면

 

 

func inner() -> Int {
    num += 1
    return num
}

 

 

 

 

정의되지 않은 num에 1을 더해주고, 그 값을 반환하고 있는 것처럼 보이기 때문에
비정상적으로 보일 수 있습니다.




하지만 사실 inner 함수는 자신을 둘러싼 함수로부터
num을 캡처하고 함수 내에서 사용하고 있는 것입니다.



이것을 클로저에 의해 값이 캡처되었다 라고 표현합니다.





 

 

 

 

 

 

2. 캡처 방법

 

 

그렇다면 클로저는 값을 어떻게 캡처할까요?

 

Swift 공식문서

 

 

공식 문서를 살펴보면 reference, 즉 참조를 캡처한다는 것을 알 수 있습니다.

그렇다면 value타입의 값도 reference 캡처를 하는 것일까요?

결론부터 말하자면, 그럴 수도 있고 아닐 수도 있습니다.

 

 

 

어떤 경우에 참조를 캡처하는지 예시를 통해서 알아보겠습니다.

 

 

func outer() -> () -> Int {
    var num = 0
    print("1: \(num)")

    func inner() -> Int {
        return num
    }

    num = 20
    print("2: \(num)")
    return inner
}

let result = outer()
print("3: \(result())")

 

 

inner 함수를 실행하기 전에 num을 0에서 20으로 변경해 줬습니다.

 


그리고 inner 함수를 실행해 보면

 

num 값이 변경된 것을 확인할 수 있습니다.

 

 

함수 실행

 

 

 

이를 통해 참조를 캡처했다는 것을 알 수 있습니다.

 

 

 

 

 

그렇다면 어떤 경우에 참조를 캡처하지 않을까요?

만약 아래에 해당하는 경우라면 값의 복사본을 캡처하고 저장할 수 있습니다.

1. 값이 클로저에 의해 변경되지 않고
2. 클로저가 생성된 후 값이 변경되지 않는 경우

 

 

 

 

이 내용 역시 예시를 통해서 알아보겠습니다.

아래 예시는 num을 상수로 변경해서 값을 변경하지 못하도록 했습니다.

 

 

func outer() -> () -> Int {
    let num = 0
    print("1: \(num)")

    func inner() -> Int {
        return num
    }

    print("2: \(num)")
    return inner
}

let result = outer()
print("3: \(result())")

 

 

 

 

이럴 경우, 클로저가 캡처할 때 num의 복사본을 캡처하고 저장하여 내부에서 사용할 수 있습니다.

따라서 무조건 reference를 캡처한다고 말하기는 어렵습니다.

 

 

 

 

 

 

 

 

 

 

 

3. 캡처 리스트

 

 

캡처 리스트는 다음과 같이 대괄호를 둘러싸여 콤마로 구분해서 작성해 줍니다.

이때 in 키워드를 반드시 함께 작성해야 합니다.

 

 

var a = 0
var b = 0

let closure = { [a] in
    print(a, b)
}

 

 

 

이렇게 캡처 리스트를 사용하면 클로저에서 값이 캡처되는 방법을 명시적으로 제어할 수 있습니다.

 

 

 

명시적으로 제어할 수 있다는 것이 어떤 의미일까요?

타입별로 예시를 통해서 알아보도록 하겠습니다.

 

 

 

 

3-1. value 타입

 

 

위에서 알아본 것처럼 value타입을 변수로 설정하고

값을 변경하면 클로저 내부에서는 reference를 캡처합니다.

 


하지만 캡처 리스트를 사용하면 어떨까요?

 

 

var a = 0
var b = 0
let closure = { [a] in
    print(a)
    print(b)
}

a = 10
b = 10
closure()

 

 

 

위 예시를 보면 클로저 내부에서 a와 b를 모두 사용하고 있지만

a는 캡처 리스트를 통해 캡처되고, b는 그렇지 않습니다.

 

 

 

이렇게 캡처 리스트를 사용하면 클로저가 생성될 때 초기화되기 때문에
생성 후에 값이 변경되어도 영향을 받지 않습니다.

 

 

따라서 a는 클로저가 생성될 당시의 값을 캡처하지만,

b는 클로저가 호출될 당시의 값을 사용하게 됩니다.

 

 

 

결과적으로 출력해 보면 a는 0, b는 10으로 출력되는 것을 확인할 수 있습니다.

 

 

a와 b 출력

 

 

 

 

 

 

 

 

 

 

3-2. reference 타입

 

 

그렇다면 reference타입의 경우에도 캡처 리스트를 사용하면 영향을 받지 않을까요?

예시를 통해서 확인해 보겠습니다.

 

 

 

class SimpleClass {
    var num: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
    print(x.num)
    print(y.num)
}

x.num = 10
y.num = 10
closure()

 

 

 

 

SimpleClass 클래스를 만들고 인스턴스를 생성하고, 변수에 할당했습니다.

 


그리고 x를 캡처 리스트를 통해서 캡처했습니다.

 

 

실제로 출력된 내용을 보면 캡처리스트를 통해 캡처해도

 value 타입과 다르게 영향을 받는 것을 알 수 있습니다.

 

 

x.num과 y.num 출력



결과적으로 캡처 리스트에 있는 x와 변수 x모두 동일한 객체를 참조하는 것을 알 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

4. 클로저의 강한 참조

 

 

 

그렇다면 캡처 리스트는 reference 타입에서 왜 사용하는 것일까요?

가독성 등 여러 이유가 있겠지만 오늘 알아볼 내용은 순환 참조 문제 해결을 위해 사용할 수 있다는 내용입니다.

 

 

(이 내용은 ARC와 관련이 있기 때문에 ARC에 대한 학습이 필요합니다.
아직 ARC 포스팅을 작성하기 전이니 최대한 간략하게 작성해 보겠습니다.)

 

 



클래스 인스턴스가 메모리에서 해제될 때 호출되는 소멸자 deinit을

사용해서 메모리에서 잘 해제되는지 확인해 보겠습니다.

 

 

class Person {
    var name: String

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) deinit")
    }
}

var minSu: Person? = Person(name: "민수")

 



Person 클래스를 만들고 인스턴스를 생성한 뒤 minSu 변수에 할당했습니다.

 

그리고 minSu를 nil로 변경해서 Person 인스턴스를 참조하지 않도록 하면

 

 

minSu = nil

 

 

 

Person 인스턴스의 RC가 0이 되어서 Person 인스턴스가 메모리에서 해제됩니다.

따라서 민수 deinit가 출력되는 것을 확인할 수 있습니다.

 

 

민수 deinit 출력

 

 

 

그런데 만약에 클로저가 self를 통해서 Person 인스턴스 프로퍼티에 접근하면 어떻게 될까요?

 

 

class Person {
    var name: String
    var introduce: (() -> Void)?

    init(name: String) {
        self.name = name
        self.introduce = {
            print("제 이름은 \(self.name)입니다.")
        }
    }

    deinit {
        print("\(name) deinit")
    }
}

var minSu: Person? = Person(name: "민수")
minSu = nil

 

 

 

이럴 경우 민수 deinit가 출력되지 않고, 메모리에서 해제되지 않는 것을 알 수 있습니다.

 

 

 

이는 Person 인스턴스와 클로저 사이에 강한 참조 사이클이 생성되기 때문입니다.

 

 

강한 참조 사이클

 

 

 



이 문제를 해결하기 위해서는 캡처 리스트를 사용할 수 있습니다.

캡처 리스트를 사용하여 약한 참조 또는 미소유 참조로 캡처하도록 할 수 있습니다.

 

 

 

다음은 약한 참조를 사용한 예시입니다.

 

 

class Person {
    var name: String
    var introduce: (() -> Void)?

    init(name: String) {
        self.name = name
        self.introduce = { [weak self] in
            guard let self = self else { return }
            print("제 이름은 \(self.name)입니다.")
        }
    }

    deinit {
        print("\(name) deinit")
    }
}

var minSu: Person? = Person(name: "민수")
minSu = nil

 

 

 

 

 


 

 

 

 

 


이렇게 클로저 두 번째 포스팅이 마무리되었습니다!

 

다음 포스팅에서는 탈출 클로저 등에 대해서 알아보겠습니다!