Swift

[Swift] 클로저와 중첩함수의 캡처 현상 (2) | 캡처리스트 클로저에서self키워드

하이D:) 2023. 11. 9. 20:32

이전 글에서 클로저의 간략한 내용캡처 현상이 일어나는 이유! 캡처 현상이 일어나는 상황들! 그리고 값 타입과 참조타입에 대해 캡처현상이 일어날 때 각각 어떻게 캡처하고 있는지에 대해 알아보았죠.

 

[Swift] 클로저와 중첩함수의 캡처 현상 (1)

[Swift] 클로저와 중첩함수의 캡처 현상 (2) | 캡처리스트 클로저에서self키워드

 

이번 글에서는 클로저에서 사용하는 캡처리스트에 대해서 알아볼 건데요!

캡처 리스트를 사용하면 클로저의 캡처현상에 대해 어떠한 변화가 생기는데 여기서도 값타입과 참조타입의 변화가 각각 다른데 이 부분이 흥미로웠어요. 

그리고 우리는 보통 클로저를 객체 내에서 사용하게 되고 이때 "힙영역"에 저장된 클로저가 "힙영역"에 저장된 객체를 강한 참조 하게 되는 상황이 있는데요. 어떤 상황일까요? 그리고 강한 순환 참조를 방지하기 위해 캡처리스트를 어떻게 사용하면 될지도 이 글의 중요한 포인트가 될 것 같습니다!

 

 

# 🥨 캡처리스트 사용 방법

클로저에서 사용한다고 한 캡처 리스트는 어떻게 사용하는 것인지 일단 한번 알아봅시다!

파라미터가 없는 경우에는 { [ 캡처리스트 ] in } 이렇게 사용하면되고,

파라미터가 있는 경우에는 { [ 캡처리스트 ] (파라미터) -> 리턴형 in  } 이렇게 사용하면 됩니다 :)

 

# 🥨 값 타입과 참조 타입의 캡처리스트 

이전 글에서 값 타입과 참조 타입에 대한 캡처가 어떻게 일어나는지 차이점을 알아보았는데

캡처 리스트에서도 대괄호 [] 에 값타입을 넣느냐 참조타입을 넣느냐에 따라 다릅니다! 그 차이점을 한번 볼까요?

 

> 값 타입의 캡처리스트

클로저가 값 타입을 캡처할 때는 메모리 주소를 캡처한다고 했는데 이러한 값타입을 캡처 리스트에 넣게 되면 어떻게 될까요?

 

"값을 복사"해서 외부적인 요인에 의한 값 변경을 방지할 수 있게 해 줍니다!

 

밑에 캡처 리스트에 값타입 넣은 코드 예제를 보면 중간에 다른 값을 할당해 주어도 이전 값(클로저의 캡처리스트에 넣기 전의 값)을 기억하고 있다는 것을 알 수 있죠!

var num = 1

let valueCaptureClosure = { [num] in
	print("num => ", num)
}

valueCaptureClosure(num) //1

num = 3

valueCaptureClosure(num) //1

 

 

> 참조 타입의 캡처리스트

클로저가 참조 타입을 캡처할 때는 인스턴스를 할당한 변수를 캡처한다고 했는데 이러한 참조타입을 캡처 리스트에 넣게 되면 인스턴스를 어떻게 참조하는지만 바뀌는데요. 참조 타입을 캡처리스트에 넣었을 경우에는 인스턴스를 직접 참조(직접캡처)하게됩니다.

 

 

그렇기 때문에 값타입을 캡처리스트에 넣었을 때와 다르게 참조타입은 캡처리스트를 사용하든 안 하든 인스턴스 내부 프로퍼티 값이 변하는 건 캡처하고 있을 수밖에 없겠죠?

class Class1 {
	var num = 0
} 

var x = Class1()
var y = Class1()


let refTypeCapture = { [x] in
	print("참조 타입 캡처", x.num, y.num)
}

refTypeCapture() //0,0

x.num = 1
v.num = 1

refTypeCapture() //1,1

 

 

 

# 🥨  캡처리스트를 사용하는 이유

위에서 본 것처럼 캡처리스트를 사용했을 때에도 값타입에 대한 캡처와 참조타입에 대한 캡처가 다르게 나타납니다. 그럼 캡처리스트를 사용하는 이유는 무엇으로 정의할 수 있을까?

값타입은 캡처리스트를 사용함으로써 값을 복사해 놓기 때문에 중간에 캡처하고 있는 변수의 값이 변경되는 걸 방지해 줄 수 있고,

참조타입은 캡처리스트를 사용해도 참조타입의 속성 값이 변한 건 계속 캡처하고 있기 때문에 값 변경 방지를 위해서가 아니라 캡처 리스트 내에서 weak(약한) / unowned(비소유) 참조가 가능하기 때문에 캡처 리스트를 사용하는 거랍니다.

 

 

# 🥨 객체 내에서 클로저 사용

우리가 프로젝트를 진행할 때에는 보통 클로저를 객체 내에서 사용하게 될 겁니다. 이때 주의해야 할 점은 무엇일까요?

 

우선, 클로저 내에서 객체의 속성 및 메서드에 접근 시에는 "self" 키워드를 반드시 사용해야 합니다.

이렇게 클로저 내에서 "self" 키워드를 사용하게 되면 해당 객체에 대한 RC(Reference Count)가 1 올라가게 되는데 이때 강한 참조를 하게 되기 때문에 강한 참조 사이클을 발생할 여지가 있는데요.

class Person {
    let name = "홍길동"
    
    func sayMyName1() {
        // ⭐️ 강한참조
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
    
}

 

 

앞선 글에서 메모리 관리에 대해 설명할 때 강한 순환 참조가 메모리 관리에 주는 위험성에 대해 알아본 바 있습니다!

https://heidi-dev.tistory.com/21

 

[Swift] 메모리 관리 (1) | ARC와 메모리 누수 (feat.강한순환참조)

Swift 메모리에 대해서 공부를 하다 보면 자연스레 ARC라는 단어가 익숙해지게 되죠. 메모리 관리는 왜 필요한지, ARC 시스템은 어떤 원리인지와 메모리 누수가 일어나는 상황에 대해서 정리해 보

heidi-dev.tistory.com

 

그렇다면 이러한 강한 참조를 해결하고 RC 가 올라가지 않게 참조해 줄 수 있는 방법은 무엇일까요??

 

 

# 🥨 캡처리스트 내 weak / unowned 키워드

위에서 말씀드린 캡처리스트 사용이유처럼 참조 타입을 캡처 리스트 내에 사용할 때는 캡처 리스트 내에서 weak(약한) / unowned(비소유) 참조가 가능해집니다. 즉, 객체 내에서 사용되는 클로저에서 객체의 속성값을 사용하기 위해 self키워드를 사용하더라도 강한 참조가 아니라 RC를 올리지 않도록 약한 또는 비소유 참조가 가능해지는 것이죠.

 

class Person {
    let name = "홍길동"
    
    func sayMyName1() {
        // ⭐️ 강한참조
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
    
    func sayMyName2() {
    	// ⭐️ 캡처리스트 내에 weak 키워드를 사용함으로써 약한참조 가능
        DispatchQueue.global().async { [weak self] in
            print("나의 이름은 \(self?.name)입니다.")
        }
    }
    
}

let person = Person()
person.sayMyName1()
person.sayMyName2()

 

이때 guard문을 사용해서 객체가 없다면 얼리 리턴하도록 사용되곤 합니다.

 

class Person {
    let name = "홍길동"
    
    func sayMyName3() {
// ⭐️ 이렇게 가드문으로 객체 없을 때 얼리 리턴하도록 많이 사용된다 
        DispatchQueue.global().async { [weak self] in
            guard let weakSelf = self else { return }
            print("나의 이름은 \(weakSelf.name)입니다.")
        }
    }
}


let person = Person()
person.sayMyName3()

 

 


 

이번 글에서는 캡처 리스트에 대해서 알아보았는데요.

클로저는 보통 객체 내에서 많이 사용되기 때문에 객체의 속성이나 메서드를 사용하기 위해 self 키워드를 사용함으로써 발생하는 강함 참조를 관리해주어야 하죠. 이때 해당 객체(참조타입)를 weak/unowned 키워드와 함께 캡처리스트에 넣어줌으로써 RC를 올리지 않고 객체를 참조해 줄 수 있다! 이 점이 이 글의 핵심이 될 것 같네요 :)