본문 바로가기

iOS/Swift

Swift) 클로저(Closure) 정복하기(3/3) - 클로저와 ARC

 

 

 

안녕하세요 :) 소들입니다!

이번 포스팅은 클로저 정복하기 마지막 편!!!

 

메모리 ARC에 대한 사전 지식이 없으면

조금 이해하기 어려울 수 있으니,

메모리 관련 포스팅을 먼저 보고 오심을 추천 드립니다 :D

 

모든 포스팅은 편의 말투로 합니다~!!

 

 

 

 

1. 클로저에서 값을 캡쳐한다는 것은

 

여러분 클로저를 사용하다보면, 값 캡쳐라는 단어를 참 많이 볼 수 있음!

근데 이 캡쳐가 도대체 뭔데???

 

이를 알기 위해서 클로저의 기본 개념을 보면,

(내 포스팅 발췌)

 

Closure란 내부 함수와 내부 함수에 영향을 미치는 주변 환경을 모두 포함한 객체이다.

 

뭐 그럼!!! 정의는 참 딱딱하니 예제로 보여드리겠음 :)

자, 다음과 같은 코드가 있을 때

 

 

func doSomething() {
    var message = "Hi i am sodeul!"
 
    //클로저 범위 시작
    
    var num = 10
    let closure = { print(num) }
 
    //클로저 범위 끝
    
    print(message)
}
 

 

 

closure란 익명함수는,

클로저 내부에서 외부 변수인 num이라는 변수를 사용(print)하기 때문에

num의 값을 클로저 내부적으로 저장하고 있는데,

이것을 클로저에 의해 num의 값이 캡쳐 되었다 라고 표현함

 

message란 변수는 클로저 내부에서 사용하지 않기 때문

클로저에 의해 값이 캡쳐되지 않음!

 

자, 이제 캡쳐가 무엇인지 알았길 바라며,

이번엔 클로저의 값 캡쳐 방식에 대해 알아야 함!!!!

 

 

 

1-1. 클로저의 값 캡쳐 방식

 

결론부터 말하겠음

 

Closure는 값을 캡쳐할 때

Value/Reference 타입에 관계 없이 Reference Capture 한다

 

음 이게 무슨 말이냐면,

아까 num이란 변수를 클로저 내부적으로 저장한다고 했잖음?

근데 num은 Int 타입의 구조체 형식이고, 이는 곧 Value 타입이기 때문에, 

값을 복사해서 들고 저장해야 되는 것이 일반적임!!!!

 

그러나, 클로저는 Value/Reference 타입에 관계없이 캡쳐하는 값들을 참조함!!

이것을 Reference Capture라고 함

 

예로 보여드리면,

 

 

func doSomething() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = {
        print("num check #3 = \(num)")
    }
    
    num = 20
    print("num check #2 = \(num)")
    closure()
}
 

 

 

먼저, closure는 num이라는 외부 변수를 클로저 내부에서 사용하기 때문에

num을 캡쳐할 것임! 근데 이때, 어떻게 캡쳐한다?

Reference Capture 즉, num이란 변수를 참조함!

 

따라서, closure를 실행하기 전에 num이란 값을 외부에서 변경하면,

 

 

 

 

클로저 내부에서 사용하는 num의 값 또한 변경 됨!

혹은, 클로저 내부에서 num의 값을 바꾸면

 

 

func doSomething() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = {
        num = 20
        print("num check #3 = \(num)")
    }
    
    closure()
    print("num check #2 = \(num)")
}
 

 

 

클로저 외부에 있는 num의 값도 변경이 됨!!!

 

 

 

 

요렇게 👀

이렇듯, Closure는 값의 타입이 Value건 Reference건 모두 

Reference Capture를 한다는 사실

 

그럼 만약 나는 Value Type으로 Capture를 하고 싶으면 어떻게 할까??👀

 

 

 

 

2. 클로저의 캡쳐 리스트 (Capture Lists)

 

 

 

 클로저의 시작인 { 의 바로 옆에  []를 이용해 캡쳐할 멤버를 나열한다

이때 in 키워드도 꼭 함께 작성한다

 

 

 

2-1. Value Type의 값을 복사해서 Capture 할 순 없나요?

 

가능함!

지금 공부하고 있는 Capture Lists 라는 것을 이용하면 할 수 있움!

Value Type의 경우, Value Capture 하고 싶은 변수를 리스트로 명시해주는 것임 !!! :)

 

 

예제로 보면

 

 

func doSomething() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = { [num] in
        print("num check #3 = \(num)")
    }
    
    num = 20
    print("num check #2 = \(num)")
    closure()
}
 

 

 

closure를 실행하기 전에 외부 변수 num의 값을 20으로 변경했지만,

 

 

 

 

클로저의 num에는 영향을 주지 않음!!!

근데, 한 가지 더 유의해야 할 점은 Value Type으로 캡쳐한 경우,

 

Closure를 선언할 당시의 num의 값을 Const Value Type으로 캡쳐함

 

자, 여기서 중요한 것은 Const Value Type,

 "상수"로 캡쳐된다는 것임

 

따라서 다음과 같이

 

 

 

 

closure 내부에서 Value Capture된 값을 변경할 수 없음

 

정리하자면,

클로저는 기본적으로 Value Type의 값도 Reference Capture를 하지만,

클로져 캡쳐 리스트를 이용하면 Const Value Type으로 캡쳐가 가능 하구나!!

 

 

 

2-2. Reference Type의 값을 복사해서 Capture 할 순 없나요?

 

자, 위에서 Value Type의 값을 클로저 "캡쳐 리스트"를 통해서 

Value Capture 하는 것 까진 잘 봤음

 

그럼 Reference Type의 값도 Capture Lists에 작성하면, Value Capture가 될까?

 

 

class Human {
    var name: String = "Sodeul"
}
 
var human1: Human = .init()
 
let closure = { [human1] in
    print(human1.name)
}
 
human1.name = "Unknown"
closure()

 

 

자 이런 코드가 있을 때, human1이라는 인스턴스는 Reference Type임

근데 내가 클로저 캡쳐 리스트를 통해 human1을 캡쳐 했으니까,

human1은 복사되어 캡쳐 됐을까? 결과 값을 보면 알 수 있음

 

 

 

 

아니 (단호)

캡쳐 리스트를 작성한다고 해도, Reference Type은 Reference Capture를 함

그럼 Reference Type은 클로저 캡쳐 리스트를 작성할 필요가 없겠네!?!?

싶겠지만, 이건 클로저와 ARC를 보면 언제 쓰는지 이해할 수 있음

 

 

 

 

3. 클로저와 ARC

 

자, 클로저 캡쳐 리스트를 더 자세히 알아보기 위해

클로저와 ARC에 대해 다뤄보려고 함 :)

 

자, 먼저 ARC는 뭐다??

인스턴스의 Reference Count를 자동으로 계산하여 메모리를 관리하는 방법이다!!

 

그렇다면 더 세세하게 얘기하자면

클로저 인스턴스간의 관계에 대해 얘기해보려고 함!

 

자, 다음과 같이 내가 Human이란 클래스를 만들고,

name을 얻을 수 있는 Lazy 프로퍼티를 클로저를 통해 초기화 했음

(이때, Lazy로 선언하지 않으면 에러가 발생하는데, 이유가 궁금하신 분이 있다면 댓글 주세요 :)

현재 포스팅과 별도의 얘기라 여기선 스킵함)

 

 

class Human {
    var name = ""
    lazy var getName: () -> String = {
        return self.name
    }
    
    init(name: String) {
        self.name = name
    }
 
    deinit {
        print("Human Deinit!")
    }
}
 

 

 

그리고 다음과 같이

 

 

var sodeulHuman= .init(name: "Kim-Sodeul")
print(sodeul!.getName())

 

 

sodeul이란 인스턴스를 만들고,

클로저로 작성되어 있는 getName이란 지연 저장 프로퍼티를 호출했음

 

그리고 나서 더이상 sodeul이란 인스턴스가 필요 없어서

 

 

sodeul = nil

 

 

이렇게 인스턴스에 nil을 할당 했음!

그럼 인스턴스에 nil이 할당 되었고, 나는 이 인스턴스를 다른 변수에 대입한 적 없고,

따라서 인스턴스의 RC가 0이 되어 deinit이 호출되어야 할 것 아님??

 

 

 

 

근데, deinit 함수는 불리지 않음 😱

왜냐???? 이제 왜인지 알아보러 가자 :) 비밀은 클로저에 있음

 

 

 

3-1. 클로저의 강한 순환 참조

 

자, 먼저 클로저는 참조 타입으로, Heap에 살고 있음!
따라서, 내가 생성한 human이란 인스턴스는, 

 

 

print(sodeul!.getName())

 

 

getName을 호출하는 순간 getName이란 클로저가 Heap에 할당되며, 이 클로저를 참조할 것임!

(지연 저장 프로퍼티니 인스턴스 생성 직후가 아닌, 호출되는 순간에 메모리에 올라감)

근데, getName이란 클로저를 보면

 

 

class Human {
    lazy var getName: () -> String = {
        return self.name
    }
}
 

 

 

이렇게 self를 통해 Human이란 인스턴스의 프로퍼티에 접근하고 있음!!

클로저는 Reference 값을 캡쳐할 때 기본적으로 "strong"으로 캡쳐를 함

따라서, 이때 Human이란 인스턴스의 Reference Count가 증가해 버림..!!!

 

이해감?? 

Human 인스턴스는 클로저를 참조하고,

클로저는 Human 인스턴스(의 변수)를 참조하기 때문에

 

서로가 서로를 참조하고 있어서

둘 다 메모리에서 해제되지 않는 강한 순환 참조가 발생해 버린 것임!

 

그럼 어떻게 할까?

ARC에서 배웠지만, 강한 순환참조는 weak, unowned를 통해

해결할 수 있다고 했음!

 

 

 

3-2. 클로저의 강한 순환 참조 해결법

 

클로저에서 해결하려면 앞서 공부한 weak & unowned

우리가 Reference Type일 땐 필요 없다 느꼈던 캡쳐 리스트를 이용해야 함

 

weak & unowned

+

Capture Lists

 

이 두가지를 이용해서 강한 순환 참조를 해결하는 것임 :)

클로저가 프로퍼티에 접근할 때 self를 참조하면서 문제가 발생 했잖음??

따라서 self에 대한 참조를 Closure Capture Lists를 이용해 weak, unowned 캡쳐해버리는 것임!

 

 

class Human {
    lazy var getName: () -> String= { [weak self] in
        return self?.name
    }
}
 

 

class Human {
    lazy var getName: () -> String = { [unowned self] in
        return self.name
    }
}
 

 

 

이런 식으로 weak, unowned로  Reference Capture를 해버리는 것임!!!

이렇게 클로저 리스트를 통해 강한 순환 참조를 해결해 줄 수 있고,

그러면

 

 

 

 

deinit이 정상 실행됨 :)

 

 

근데,

weak의 경우 nil을 할당받을 가능성이 있기에 Optional-Type으로

self에 대한 Optional Binding을 해주어야 하지만,

 

unowned의 경우엔 Non-Optional Type으로

self에 대한 Optional Binding 없이 사용할 수 있음!!!

(물론 Swift 5.0부턴 unowned도 Optional Type이 되지만,

캡쳐 리스트로 동작할 땐 Non-Optional Type으로 동작 하는듯..!!!?)

 

ARC를 공부할 때

unowned를 도대체 언제 사용하냐????? 란 의문이 들었었는데,

위와 같이 클로저를 Lazy Initialization로 선언해서 강한 순환 참조가 일어난 경우엔,

 인스턴스가 존재해야만 (접근하여) 초기화를 시킬 수 있고, 따라서 이때 self는 값이 있다고 가정하기 때문에,, 

이 경우엔 unowend 를 사용하는 것이 가능하고,

unowned를 사용할 경우 Optional Binding을 하지 않아도 돼서 코드도 깔끔해 짐!!

(아닌가..? 내가 이해한 게 아니라면 피드백 댓글 주세요..ㅠㅠ)

 

 

+ 라고 생각 했으나...

댓글 피드백을 보니 만약 Lazy로 선언된 클로저가 self를 unowned로 캡쳐하고,

시점 차이로 인해 해당 인스턴스에 nil이 할당 된 후에도

lazy property 작업이 실행되어야 하는 상황이 생길 경우,

이때는 unowned self capture에 문제가 있어 보입니다!!

따라서............... weak 권장요 ㅎ..

 

 

 

 

4. Swift에서 클로저는 여러 개니까

 

그것을 알기 전에, 먼저 내가 이전에도 누누히 말했듯

 

 

클로저는 

전역 함수, 중첩 함수, 익명 함수

이 세가지를 모두 아우르는 말이라 했음!!!

(물론 일반적으론 익명 함수를 칭하긴 하지만!!)

 

근데 우린 바로 위에서 Unnamed Closure, 즉 익명함수일 때만 값 캡쳐 방식을 살펴봤단 말임?

따라서 Named Closure일 때 값 캡쳐하는 방식을 살펴볼 것임

 

 

 

4-1. 전역 함수

 

우리가 일반적으로 func 쓰고 작성하는 함수를 말하는데 (중첩 X)

이 전역 함수는 주변의 어떠한 값도 캡쳐하지 않음!

 

 

 

4-2. 중첩 함수

 

자신을 포함하고 있는 함수의 값을 캡쳐함!

예로 보면,

 

 

func outer() {
    var numInt = 0
    
    func inner() {
        print(num)
    }
}

 

 

이런 함수가 있으면

inner 함수는 나를 포함하고 있는 함수 outer의 num이라는 값을 사용하니 캡쳐

(당연히 Reference Capture)

 

 

 

 

5. 중첩 함수 & @escaping과 메모리의 관계

 

자, 이전 포스팅에서 의문을 풀기위해 두둥등장

우리가 함수 파라미터를 받을 때 @escaping이란 키워드 없이 받는 클로저는 모두

non-escaping 클로저이고, 따라서 다음과 같은 특징을 가진다고 했음

 

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

따라서 파라미터로 받은 클로저는 변수나 상수에 대입할 수 없고,

중첩 함수 내부에서 클로저를 사용할 경우, 중첩함수를 리턴할 수 없다

함수의 실행 흐름을 탈출하지 않아, 함수가 종료되기 전에 무조건 실행 되어야 한다

 

자 여기서 정리하자면 non-escaping 클로저는

변수나 상수에 대입 불가, 중첩함수 내부에서 사용 시 중첩함수 리턴 불가, 함수 종료 후 실행 불가

이렇다는 거잖음???????? 이런 조약이 왜 붙은지 아셈??

클로저가 함수 외부로 탈출하지 못하게 하기 위해서

 

무슨 말이냐,

non-escaping 클로저는 해당 클로저가 함수가 종료되기 직전에 무조건 실행이 된다는 조건임

근데 만약 변수나 상수에 대입할 경우, 해당 클로저가 변수나 상수로 함수에서 리턴될 수도 있고,

중첩 함수 내부에서 클로저를 사용한 경우, 중첩 함수가 클로저를 캡쳐하기 때문에

중첩 함수를 리턴 시 클로저가 중첩 함수에 의해 함수 외부에서 실행될 수 있기 때문임!!

 

 

 

 

이런 식으로 closure를 사용하는 inner가 리턴되어 버리면

외부에서 inner를 받고 실행하면, closure가 호출되어야 하니까!

(물론 위 코드는 에러뜸)

 

 

 

3-1. 왜 non-escaping과 escaping을 나눴을까

 

편하게 다 escaping 클로저로 선언하면 되지 않나? 라고 생각할 수 있겠지만,

non-escaping에 왜 함수 직전에 무조건 실행 되어야 하는 조건이 붙냐면,

클로저가 이 함수 내부에서만 쓰이기 때문에 컴파일러가 메모리 관리를 

지저분하게 하지 않아도 돼서, 성능이 향상되기 때문임

 

non-escaping의 경우 함수가 종료됨과 동시에 클로저도 사용이 끝나지만,

escaping의 경우, 함수가 종료되더라도 실제 클로저가 사용되지 않을 때까지 메모리를 추적해야 함

 

궁금증이 풀렸길 .. :)

 

 

 

 

 

.

.

.

 

클로저 정복하기도 끝... :)

ARC가 나와서 좀 어려울 수도 있는데.....

혹여나 틀린 내용이나 궁금증, 피드백 언제나 환영이빈다 👀🌸



Calendar
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
최근 댓글
Visits
Today
Yesterday