iOS/Swift

클로저 6편

밤새는 탐험가89 2024. 2. 17. 22:35

 

 

@escaping 

 

지금까지 써온 클로저는 다음과 같이 "non-escaping Closure" 임 

 

 

func runFuntion(closure: (Int, Int) -> Int) {
    closure(1,2)
}

 

 

 

non-escaping Closure 란?

 

함수 내부에서 직접 실행하기 위해서 사용

 

파라미터로 받은 클로저를 변수 또는 상수에 대입 불가 

 

중첩 함수에서 클로저를 사용할 경우

중첩 함수 리턴 불가 

 

함수의 실행 흐름에서 탈출 안되기 때문에 

함수가 종료되기 전에 무조건 실행되어야 함 

 

 

 

만약에 상수에 클로저를 대입하면?

func runFuntion(closure: (Int, Int) -> Int) {

    let answer: (Int, Int) -> Int = closure
    
}

 

 

 

아래와 같이 오류가 발생

 

 

 

 

 

함수의 실행 흐름에서 탈출 할 수 없다는 말은 

함수가 종료된 후에 클로저가 실행되면 오류가 발생 

 

 

 

이렇게 함수 실행을 벗어나서 함수가 끝나고 난 후에 클로저를 실행하거나 

중첩함수에서 실행 후 중첩 함수를 리턴 또는 변수나 상수에 대입하고 싶을 때 

 

 

 

"@escaping" 키워드를 사용

 

클로저 파라미터 타입 앞@escaping 키워드 사용

 

func runFuntion(closure: @esacping (Int, Int) -> Int) {
    closure(1,2)
}

 

 

 

"@escaping" 클로저를 사용할 경우 주의해야 할 점이 

 

함수가 종료된 후에 클로저를 실행할 때,

클로저가 함수 내부의 값을 사용해야 하기 때문임

 

 

 

 

 

값 캡쳐

 

closure는 내부 함수와 내부 함수에 영향을 미치는 

주변 환경을 모두 포함한 객체를 의미함 

 

func runFunction() {
    let title: String = "Start"

    let num: Int = 100
    let closure = {
        print(num)
    }

    closure() 
    print(title)

}

 

 

"closure" 라는 내부에서 외부 변수인 num이라는 것을 받아서 print() 하고 있는데

이를 num 의 값을 클로저 내부적으로 저장하고 있다 또는 num의 값을 캡쳐했다라고 함 

 

 

 

여기서 title 은 클로저 내부에서 사용하지 않기 때문에

closure에 의해 값이 캡쳐되지 않음 

 

 

 

 

 

클로저의 값 캡쳐 방식 

 

클로저는 값을 캡쳐할 때 

참조 방식(Reference Capture)로 함

(값 타입이어도 참조 타입으로 캡쳐함)

 

 

위에서도 num 이라는 Int 타입은 구조체 형식, 즉 값 타입이지만, 

클로저에서는 이를 참조 타입으로 저장함 

 

 

func runFunction() {
    
    var title: String = "title"
    print("title is \(title)")

    let closure = {
        print("title is \(title)")
    }

    title = "title changed"
    print("title is \(title)")
    
    closure()

}

runFunction()

 

 

위의 함수에서 보면 title 이라는 변수의 값을 변경하면 

closure는 title 외부 변수를 클로저 내부에서 참조방식으로 캡쳐하기 때문에 

아래와 같이 결과가 나옴

 

 

 

 

또한, 클로저 내부에서 title의 값 변경 가능한데, 

이렇게 변경하면 다음과 같이 나옴 

 

 

func runFunction() {
    
    var title: String = "title"
    print("title is \(title)")

    let closure = {
        title = "changed in closure"
        print("title is \(title)")
    }

    closure()
    
    print("title is \(title)")

}

runFunction()

 

 

closure 내부에서 title 이라는 변수의 값을 변경하면

외부 변수의 값도 변경

 

즉, 참조 타입으로 값을 캡쳐함 

(값 타입이어오 참조 타입으로 함)

 

 

 

추가로 closure는 호출하기 전까지는 아무것도 하지 않음

 

 

 

 

즉 closure 내부에서 변수를 변경했더라도

호출하지 않으면 아직 변경되지 않음 

 

 

 

 

 

값 캡쳐할 때 Value Type으로도 가능하긴 함

아래의 방식처럼 변수를 "[ ]"로 묶어주면 됨 

 

 

 

 

 

클로저, ARC

 

※ ARC란?

메모리 관리를 위해 인스턴스의 Reference Count를 자동으로 계산해주는 기능 

 

class Movie {

    var title: String = ""
    
    lazy var checkTitle: () -> String = {
        return self.title
    }

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

    deinit {
        print("Movie Deinit!")
    }
}

 

 

 

※ 여기서 checkTitle 프로퍼티를 lazy로 선언한 이유

 

일반 변수들은 클래스가 생성된 이후에 접근이 가능하기 때문에, 

클래스 내에서는 "self"를 통해 접근 불가 

다만, "lazy" 키워드를 통해 클래스가 생성된 이후에 접근한다는 의미로

클로저 내부에서 "self"로 접근이 가능 

 

 

var topGun: Movie? = Movie(title: "Top Gun")
print(topGun!.checkTitle())            // Top Gun

 

 

 

topGun이란 인스턴스 만들고 난 후에 

인스턴스가 더 이상 필요 없어서 "nil" 할당 

 

그러면??

인스턴스의 RC가 0이 되서 deinit이 호출되야 하는데?

 

 

topGun = nil

 

 

호출 안됨

 

 

 

 

클로저의 강한 순환 참조 

 

클로저참조 타입으로 Heap에 존재 

 

topGun 이라는 인스턴스는 

checkTitle 을 호출하는 순간, 

클로저가 Heap에 할당되고 

이 클로저를 참조하게 됨 

 

 

여기서 checkTitle을 보면

self 를 통해 Movie 클래스의 title 이라는 저장 프로퍼티에 접근하고 있음 

 

 

 

클로저는 기본적으로 참조 타입의 값을 캡쳐할 때

"Strong" 으로 캡처함 

 

따라서 이 때 Movie의 RC가 증가

 

즉, 

Movie의 인스턴스 (topGun)은 클로저 (checkTitle)을 참조하고 

클로저 (checkTitle)은 Movie의 인스턴스 (topGun)을 참조하는, 

 

서로가 서로를 참조해서 

메모리에서 해제되지 않는 강한 순환 참조 발생

 

 

 

 

강한 순환 참조 해결 방법

 

weak & unowned + 캡쳐 리스트 

 

 

먼저, 클로저가 프로퍼티에 접근할 때, "self"를 참조하면서 문제가 발생했기 때문에, 

"self"에 대한 참조를 캡쳐 리스트 기능을 통해 weak, unowned로 캡쳐함 

 

 

 

 

이런 식으로 "[weak self]"를 통해 값을 캡쳐함

그러면 아래와 같이 "Movie Deinit!" 출력됨 

 

 

 

 

 

 

 

지금까지 다룬 클로저는 non-escaping 클로저임 

 

변수나 상수에 대입 불가 

중첩함수 내부에서 사용시, 중첩함수 리턴 불가 

함수 종료 후 실행 불가 

 

이러한 이유는 바로 "클로저가 함수 외부로 탈출하지 못하게 하기 위함" 임

 

 

 

이러면 모든 클로저를 다 @escaping 으로 실행하면 되지 않을까 싶지만

non-escaping 경우 함수 직전에 무조건 실행되어야만

이를 통해 클로저가 해당 함수 내부에서만 쓰여 메모리 관리에 용이하기 때문

 

 

non - escaping

 함수가 종료되면 동시에 클로저의 사용도 끝남

 

 

escaping

 함수가 종료되더라도 실제 클로저가 사용되지 않을 때까지 추적하게 됨 

 

 

 

'iOS > Swift' 카테고리의 다른 글

MVC 패턴 (Model - View - Controller)  (0) 2024.05.25
확장 (Extension)  (0) 2024.05.24
클로저 5편  (1) 2024.02.13
클로저 4편  (0) 2024.02.10
클로저 3편  (0) 2024.02.10