[Swift] Closure

MinTa·2022년 3월 8일
0
post-thumbnail

처음에 이해하기 매우 까다로웠던 Closure에 대해서 드디어 정리해보고자 한다.


Swift의 함수는 1급 객체함수이다.

  1. 변수나 상수에 함수를 대입할 수 있다.
    이 뜻은 결과값이 대입되는 것이 아닌 함수 그 자체가 변수에 할당이 가능하다.
  2. 함수의 return값으로 함수를 사용할 수 있다.
  3. 함수의 파라미터로 함수를 전달할 수 있다.

Closure는 함수이다.

Closure란? 우선 함수를 말한다. 코드의 블럭.
(보통 익명함수(Unnamed Function을 가르키는데, 기본 함수도 사실 Named Closure라고 표현 할 수 있지만 보통 이름이 없는 함수를 Closure로 지칭한다) 이것 역시 힙 메모리(동적으로 할당되는 메모리 영역) 안에 저장된다 그래서 기본적으로 참조 타입이다. 왜냐하면 클로저는 함수이고 함수는 Swift에서는 일반 타입이다.

클로저 내부에서는 기본적으로 그 주변에 있는 범위 안의 모든 변수를 참조할 수 있다.
클로저 안에서 캡쳐가 된 모든 것들은 이미 힙안에 있다면 클로저로부터 Strong 포인터를 갖고 그렇지 않으면 힙으로 옮겨지게 되고 결국 클로저 안에 있거나 실행할때 내부적으로 참조되는 모든 것들이 힙에 저장된다.

모든 것들이 힙에 저장되게 된다면 문제가 생길 수 있는 부분이 존재한다.
바로 메모리 주기이다. 이 부분은 앞서 내가 작성한 ARC(Automatic Reference Counting)와 관련돼있다.

객체를 가리키는 클로저가 있는데 그 객체가 다시 클로저를 가리키게 되면 strong pointer로 서로를 참조하고 있어 영원히 힙에서 벗어날 방법이 없어진다.
그렇다면 이 상태에서 어떻게 하면 힙에서 해방시킬 수 있을까?
절대 없다.
왜냐하면 객체와 클로저는 서로 항상 strong 포인터를 유지하고 있기 때문에 그 포인터 중 하나를 nil로 설정하지 않는 한 방법이 없다. 하지만 접근할 방법이 없기 때문에 불가능하다.

func redSqrt(symbol: String, operation: (Double) -> Double)

위 함수를 예제로 설명을 해보자.

redSqrt("🔴", operation: {(x: Double) -> Double in
	display.textColor = UIColor.redColor()
    return sqrt(x)
}) 

closure 경량화 문법

맨뒤에 closure parameter가 온다면 trailing-closure로 아래와 같이 뺄 수 있다. 또한 swift는 타입추론이 가능하기 때문에 타입도 적어질 필요없이 두번째 인자를 빼낼 수 있다. 그리고 첫번째 인자순대로 $0으로 대체가능하기 때문에 x 또한 지워주고 $0으로 대체한다.

self..?? ( 강한 순환참조 Strong retain cycle )

redSqrt("🔴") {
	display.textColor = UIColor.redColor()
    return sqrt($0)
}

위와 같이만 한다면 self.를 넣으라고 컴파일 에러를 일으킬 것이다. 여기서 self는 ViewController를 가르킨다.

self.display.textColor = UIColor.redColor()

보통은 앞에 self를 붙여주지 않아도 클라스 프로퍼티를 참조할 수 있다. 하지만 Swift는 굳이 self를 넣으라고 컴파일 에러를 일으키는 이유는 컴파일러가 우리가 "클로저가 이 self를 캡쳐하고 이제 strong 포인터를 유지할 것이라는 것"을 인지하길 바라기 때문이다.

강한 순환참조 깨부수기

그렇다면 이런 강한 순환참조를 깨기 위한 방법으로 무엇이 있을까??
우선 Swift에는 클로저 내부에서 사용하기 위한 변수를 선언할 수 있는 방법이 있다.

redSqrt("🔴") { [me = self] in
	me.display.textColor = UIColor.redColor()
    return sqrt($0)
}

위와 같이 이렇게 대괄호([])안에 변수와 초기값을 지정해줄 수 있다. 리스트와 같이 여러개 선언해줄 수 있다.

그리고 그 변수들에 사용할 수 있는 것으로 unowned와 weak가 있다.
unowned와 weak를 사용하는 것은 reference count를 늘리지 말라고 말하는 것과 같다.
그 말은 즉 strong pointer를 만들지 않겠다는 의미이다.

redSqrt("🔴") { [unowned me = self] in
	me.display.textColor = UIColor.redColor()
    return sqrt($0)
}

이제 이 클로저는 self에 대해 strong pointer를 가지고 있지 않게 된다. 하지만 self는 여전히 클로저에 대한 strong pointer를 가지고 있다.

한가지 또 바꿀 점은 거의 불가능한 일이지만 만약 이 클로저가 ViewController보다 오래 메모리에 남아있는다면, 즉 ViewController가 사라진 후에 클로저를 실행하고자 한다면 충돌이 일어나게 될 것이다. 왜냐하면 me가 힙 메모리에 없는 것을 가르키게 되기 때문이다.

redSqrt("🔴") { [weak me = self] in
	me?.display.textColor = UIColor.redColor()
    return sqrt($0)
}

위와 같이 weak를 쓰게 된다면 weak는 힙에 아무것도 보관하지 않기 때문에 강한 순환참조와 더불어 위의 문제도 해결할 수 있다.
추가로 weak는 옵셔널이기 때문에 optional chaining을 통해 me옆에 "?" 를 붙여서 작성할 수 있다.


Stanford iOS 강의를 보며 정리함

profile
지(치지않고)꾸(준히)열(심히)

0개의 댓글