본문 바로가기
Swift

Closer에 관하여2

by iOS 개린이 2023. 3. 2.

클로저의 값 획득(Capture)

-클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 캡쳐할 수 있다.

따라서 값 캡쳐를 통해 주변에 정의한 상수나 변수가 존재하지 않더라도, 클로저 바디에서 해당 값을 수정하거나, 참조할 수 있다.

 

예제를 통해 클로저의 캡쳐가 무엇인지 알아보자.

 

클로저 capture의 바디를 보면 외부 변수인 number를 사용하고 있다.

이것을 "클로저에 의해 number값이 캡쳐되었다" 라고 표현한다.

 

 

클로저의 값 캡쳐 방식

-클로저는 값을 캡쳐할 때 Value나 Reference 타입에 관계없이 Reference 캡쳐를 실행한다.

위 예제에서 보았듯이 number는 Int타입 -> 구조체 -> Value 타입이다.

Value 타입은 값을 복사해서 저장하는 것이 일반적인데, 클로저는 이것에 관계없이 캡쳐한 값을 참조한다.

 

ex) 중첩함수를 통한 값 캡쳐

 

add함수는 Int형의 매개변수 num을 받고 있고,  adding 중첩함수 내에서 외부 변수 total의 값과 매개변수 num값을 합산한 결과를 다시 total에 넣어 반환하고있다.

add함수는 return 타입으로 ( ) -> Int 를 선언했다. 이는 Int형을 반환해주는 함수 객체를 반환해주어야 한다는 의미이다.

 

여기서 말하고자 하는 것은 adding 중첩함수에서는 외부 변수 total의 값을 캡쳐하고 있다. 즉, adding 함수가 total 변수를 참조하고 있는 것이다.

참조의 특징은 어느 한 곳에서 값을 변경하면, 원본이 같이 변경되는 것이죠?

따라서 adding 함수 내에서 참조중인 total 값에 변화를 주면 total 값도 같이 변경이 된다.

 

 

원래 예상대로라면 계속 1값만 print 해서 나와야 하는데, 코드의 주석을 보면 결괏값이 1씩 증가한 것을 볼 수 있다.

이는 total 변수를 캡쳐해놓았기 때문에 함수가 선언될 때마다 total 값이 초기화되지 않고, 캡쳐해둔 값을 계속해서 사용한다는 것을 말한다.

 

 

값 캡쳐가 필요한 이유 

-클로저는 주로 비동기 작업에 많이 사용된다.

예를 들어 클로저를 통해 비동기 콜백을 작성하는 경우, 주변의 변수나 상수의 값을 미리 캡쳐해놓지 않으면,

클로저가 실행되는 순간에 변수나 상수는 메모리에서 이미 사라져 오류가 발생할 수 있다.

 

 

클로저의 캡쳐 리스트

-캡쳐 리스트는 클로저 내부에서 참조 타입을 획득하는 규칙을 제시해줄 수 있는 기능으로

클로저를 통한 강한 참조를 해결하기 위해서 사용할 수도 있고,

Value Type을 Value 캡쳐하고 싶을 때 사용할 수도 있다.

 

-캡쳐 리스트에서 사용한 요소들이 참조타입이 아니라면, 해당 요소들은 클로저가 생성될 때 초기화된다. (Value 캡쳐 효과)

 

캡쳐 리스트의 사용

 

캡쳐 리스트를 사용하고 싶으면 클로저의 매개변수 목록 이전 위치에 [ ] 중괄호를 통해 캡쳐할 멤버를 나열해준다.

캡쳐 리스트 뒤에는 in 키워드를 꼭 작성해준다.

 

 

캡쳐 리스트를 사용하면 Value Type의 값을 복사해서 캡쳐하는 것이 가능하다.

 

inner는 외부 변수 num을 캡쳐 리스트에 넣고, inner를 실행하기 전에 num의 값을 20으로 변경해주었다.

 

위 코드의 결괏값이 예상되시나요?

만약 캡쳐 리스트를 사용하지 않았다면, 우리의 예상은 클로저 inner가 num을 캡쳐하고 있기 때문에 num값이 변경될 때 같이 변경된 값을 print 했을 것이고, 따라서 결과는 이렇게 나왔을 것이다.

 

num check #1 = 0

num check #2 = 20

num check #3 = 20

 

하지만 결과는 이렇게 나온다.

 

아까 캡쳐 리스트의 설명에서 캡쳐 리스트에서 사용한 요소들이 참조타입이 아니라면, 해당 요소들은 클로저가 생성될 때 초기화 된다. 

num은 참조타입이 아닌 값 타입이고, inner의 캡쳐 리스트에 사용할 요소로 num을 넣어주었다.

따라서 클로저 inner는 num값을 value 캡쳐하면서 생성과 동시에 초기화 시켜준다.

 

또 inner는 캡쳐 리스트로 num값을 value 캡쳐하는 것도 맞는데, 좀 더 구체적으로 Const Value Type으로 캡쳐한다.

Const Value Type은 상수로 값을 캡쳐한다는 뜻으로 클로저 내부에서 Value  캡쳐된 값을 변경할 수 없다는 것이다.

 

ex) Const Value Type

 

정리해보면 클로저는 기본적으로 값을 캡쳐할 때 Value나 Reference 타입에 관계없이 Reference 캡쳐를 실행한다.

하지만 참조타입이 아닌 Value 타입을 Value 캡쳐를 하고 싶다면? 클로저 캡쳐 리스트를 이용하면 된다.

 

 

클로저의 캡쳐 리스트에 Value Type이 아닌 Reference Type을 넣어도 Value 캡쳐를 할까?

캡쳐 리스트에서 사용한 요소들이 참조 타입이라면?    

 

만약 closer가 값 캡쳐를 했다면, 클로저 선언 시 변수 value의 값을 복사한 0이 나와야 한다.

하지만 Reference 타입은 캡쳐 리스트에 넣어준다해도 Value 캡쳐를 하지 않고, Reference 캡쳐를 한다.

따라서 결괏값은 1이 나온다.

 

 

총 정리

1. 클로저는 기본적으로 값을 캡쳐할 때 Value나 Reference 타입에 관계없이 Reference 캡쳐를 실행한다.

2. 클로저 캡쳐 리스트를 사용하면 Value 타입의 값을 Value 캡쳐 해줄 수 있다.

3. 캡쳐 리스트를 사용하면 Reference 타입의 값도 Value 캡쳐가 가능한가?

답은 ㄴㄴ. Reference 타입은 캡쳐리스트 사용해도 Reference 캡쳐를 사용한다.

 

 

 

 

클로저와 ARC

-클래스 인스턴스의 프로퍼티로 클로저를 할당해줄 경우, 클로저는 해당 인스턴스나 인스턴스 멤버의 참조를 캡쳐할 수 있다.

여기서 클로저와 인스턴스 사이의 강한 순환참조가 발생할 수 있다.

요것에 대해서 알아보자.

 

다음과 같이 Student 클래스를 만들었다.

 

 

Student 클래스의 인스턴스를 만들어 getName 클로저를 실행하고,

용도가 끝난 클래스 인스턴스에 nil을 할당하여 deinit 메서드가 불리도록 해보자.

 

여기까지 실행했을 때,

1. print(student!.getName())의 결괏값과

2. Student 클래스의 참조횟수가 0이 되어 deinit 메서드가 실행 

이렇게 2가지가 실행될 것이라고 생각할 수 있지만 1번 print의 결괏값만 실행된다.

 

2번 deinit 메서드가 실행되지 않은 이유는? 클로저로 인해 순환 참조가 발생했기 때문이다.

 

자세하게 설명해보자면,

먼저 클로저의 캡쳐는 Reference 타입의 값을 캡쳐할 때 기본적으로 강한 참조를 한다.

1. Student의 인스턴스를 통해 getName 클로저를 실행시킴. 클로저는 참조 타입으로 Heap 영역 할당되며, Student가 클로저를 참조함.

2. getName의 바디에서 self 키워드를 통해 Student 인스턴스 프로퍼티에 접근한다. (강한 참조로 인해 Student 인스턴스 참조횟수가 +1 된다.)

 

따라서 Student 인스턴스도 getName 클로저를 강한참조, getName 클로저도 Student 인스턴스를 강한참조하고 있기 때문에 순환참조가 발생한다.

 

이 순환참조 문제를 해결하기 위해서 캡쳐 리스트와 weak, unowned를 사용한다.

 

ex) weak(약한 참조)를 통한 순환 참조 해결

 

캡쳐 리스트를 이용하여 weak self로 캡쳐해줌으로써 강한 순환 참조를 끊어낸다.

 

 

ex) unowned(미소유 참조)를 통한 순환 참조 해결

 

순환 참조를 해결해줄 수 있는 또 한가지인 unowned를 이용해줄 수도 있다.

 

 

약한 참조일 때는 옵셔널, 미소유 참조일 때는 옵셔널이 아닌 이유

weak(약한참조)는 nil을 할당받을 가능성이 있기 때문에 옵셔널 타입으로 self에 대한 옵셔널 바인딩을 해주어야 한다.

unowned(미소유 참조)는 non-optional 타입으로 self에 대한 옵셔널 바인딩이 필요없다.

 

근데 미소유 참조를 통한 캡쳐를 했을 경우에 시점 차이로 인해 해당 인스턴스에 nil이 할당된 후에도 lazy property 작업이 실행되어야 하는 상황이 생길 수도 있으므로 weak 캡쳐를 권장한다.

 

 

 

 

자동 클로저(Auto Closure)

-함수의 전달인자로 전달하는 특정 표현을 자동으로 변환해주는 것을 말한다.

-자동클로저는 전달인자를 갖지 않으며, 호출되었을 때, 자신이 감싸고 있는 코드의 결과값을 반환해준다.

 

-자동 클로저는 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않는 특징이 있어서 연산을 지연시킬 수 있다.

이 특징은 코드의 실행을 제어하기 좋아서 연산에 자원을 많이 소모하거나, 부작용이 우려될 때 유용하게 사용된다.

 

 

예제 코드를 보면 let customerProvider = { customerInLine.remove} 를 통해 배열에서 0번 인덱스를 삭제했다. 그럼 4개가 나와야 하는게 맞죠? 하지만 그 다음줄에서 배열 갯수를 세어보니 변화가 없다.

 

그리고 customerProvider를 직접 호출해주고 배열 갯수를 세어보니 변화가 이루어진 것을 볼 수 있다.

이렇듯 자동 클로저는 코드가 적혀진 순서대로 실행되지 않고, 실제 사용될 때 지연 호출이 된다.

 

 

클로저를 매개변수로 전달해줄 때

 

serve 함수는 () -> String 형의 클로저를 매개변수로 받는 함수이다.

이 함수를 자동 클로저를 사용하여 표현한다면? 

자동클로저는 함수 타입인 매개변수 정의 앞에 @autoclosure를 붙여 사용한다.

 

 

자동 클로저 표현을 사용해주니 serve 함수를 선언할 때, { } 중괄호로 감싸는 표현을 생략해줄 수 있다.

이렇게 자동 클로저는 함수로 전달하는 클로저를 ( ) 소괄호와 { } 중괄호를 겹쳐 사용해야 하는 표현을 사용하지 않고도, 클로저를 사용할 수 있도록 해준다.

 

하지만 자동 클로저를 너무 남용하면 코드를 이해하기 어려워질 수 있기에 문맥과 함수 이름이 자동 클로저를 사용하기에 분명해야 한다!

 

 

 

 

탈출 클로저( Escaping Closure)

 

Non-escaping Closure

 

-먼저 Non-escaping Closure에 대해서 알아보자.

 

위 코드는 지금까지 써왔던 클로저다. 이 클로저를 다르게 부를 수도 있는데,

그것이 바로 Non-escaping Closure이다. 

 

 

Non-escaping Closure의 특징

 

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

따라서 매개변수로 받은 클로저를 변수나 상수에 대입할 수 없고, 중첩함수에서 클로저를 사용할 경우 중첩함수를 리턴할 수 없다.

 

ex) 매개변수로 받은 클로저를 변수나 상수에 대입

 

 

ex) 매개변수로 받은 클로저를 중첩함수 내에서 사용할 경우 중첩함수를 리턴할 수 없음.

 

 

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

 

에러가 발생하는 코드는 10초 뒤에 클로저를 실행해주는 내용으로 함수가 종료되고 실행되기 때문에 에러가 발생한다.

 

 

escaping Closure

-함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출되는 것을 허용해주는 클로저이다.

 

 

escaping Closure의 특징

-Non-escaping Closure의 특징과 반대로 생각하면 된다.

 

1. 매개변수로 받은 클로저를 변수나 상수에 대입하고 싶거나 중첩함수 내에서 실행 후 중첩함수를 리턴하고 싶을 때

2. 함수의 종료 후에 클로저를 실행시키고 싶을 때

escaping Closure를 사용한다.

 

 

 

escaping Closure의 사용방법

 

매개변수 이름의 : 콜론 뒤에 @escaping 키워드를 사용해주면 된다.

이로써 Non-escaping Closure에서 불가능했던 조건들이 escaping을 통해 가능해진다.

(Non-escaping Closure의 예제들에 escaping을 붙여 실행해보면 모두 정상 실행되는 것을 볼 수 있음.)

 

 

외부 변수에도 매개변수로 받은 클로저 할당이 가능함.

 

1) closure 함수의 매개변수로 클로저가 전달된다.

2) 외부 변수인 completionhandeler에 매개변수 클로저를 저장한다.

3) closure 함수가 리턴 값을 반환하고 종료된다.

4) 매개변수 클로저는 아직 실행되지 않았다.

 

매개변수로 받은 클로저는 함수가 종료되었는데도 실행되지 않고, 외부에서 실행이 가능해진다. 

 

 

HTTP Request CompletionHandler에서 사용되는 escaping

 

위 코드를 보면 매개변수로 받은 completion 클로저는 함수 실행 중에 즉시 실행되지 않고, 

URL 요청이 끝난 후 비동기로 실행된다.

보통 클로저가 다른 변수에 저장되어 나중에 실행되거나, 비동기로 실행될 때 escaping 클로저를 사용한다.

 

 

 

Nonescaping Closure와  Escaping Closure

-@escaping 키워드를 붙였을 때는 꼭 escaping clousure의 목적에 맞게 사용해야 하나?

 

ㄴㄴ escaping clousure로 만들어 놓고, Nonescaping clousure로 사용할 수도 있다.

하지만 반대로 Nonescaping clousure를 escaping clousure로 사용할 수 있는지??

Nonescaping clousure 설명에서도 보았듯이 이건 불가능함. 

 

그럼 두 가지를 따로 구분하지 않고 모든 클로저에 @escaping을 붙여 사용하면 더 편리한거 아닌지??

답은 ㄴㄴ

Nonescaping clousure와 escaping clousure를 나누어서 사용하는 이유는 컴파일러의 퍼포먼스와 최적화 때문이다.

 

Nonescaping clousure는 컴파일러가 클로저의 종료 시점을 알 수 있기 때문에 때에 따라 클로저에서 사용하는 특정 객체에 대한 retain, release 등의 처리를 생략해 객체의 life-cycle을 효율적으로 관리할 수 있다.

반대로 escaping clousure는 클로저에서 사용하는 객체에 대한 추가적인 참조관리를 해줘야 하기 때문에 컴파일러의 퍼포먼스와 최적화에 영향을 미친다. (class 성능 향상을 위한 final과 같은 맥락인듯)

 

따라서 구분지어라~

 

 

 

 

 

 

 

Reference :

-클로저 (Closures) - The Swift Language Guide (한국어) (gitbook.io)

-야곰님의 스위프트 문법 개정 3판

-Swift) 클로저(Closure) 정복하기(2/3) - 문법 경량화 / @escaping / @autoclosure (tistory.com)

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

-[Swift] Escaping 클로저 (@escaping) – 토미의 개발노트 – iOS 개발관련 지식을 공유합니다. (jusung.github.io)

-[Swift] 클로저를 사용하는 이유, 값 캡쳐(Capturing Value) (tistory.com)

-[Swift] ARC & 순환참조 & 클로져 [인용자료] (tistory.com)

 

 

'Swift' 카테고리의 다른 글

모나드에 관하여  (0) 2023.03.10
고차함수에 관하여  (0) 2023.03.06
Closer에 관하여  (0) 2023.03.01
연산자 정리  (0) 2023.02.28
열거형에 관하여 2  (0) 2023.02.17