study record
[Swift] 클로저, 탈출 클로저와 메모리 본문
클로저란?
클로저는 코드블럭으로 어떤 상수나 변수의 참조를 캡쳐해 저장할 수 있다. 스위프트는 캡처 관련 메모리를 알아서 처리한다.
클로저의 세 가지 형태
- 전역 함수 : 이름이 있고 어떤 값도 캡처하지 않는 클로저
- 중첩 함수 : 이름이 있고 관련한 함수로부터 값을 캡쳐할 수 있는 클로저
- 클로저 표현 : 경량화된 문법으로 쓰여지고 관련된 문맥으로부터 값을 캡쳐할 수 있는 이름이 없는 클로저
클로저에서는 argument label 은 쓰이지 않고 parameter name만 쓰임
closure("Sodeul")
closure(name: "Sodeul") //error!
클로저는 익명이지만 함수이므로 1급 객체 함수의 특성을 다 가지고 있다.
- 클로저를 변수나 상수에 대입할 수 있다.
- 함수의 파라미터 타입으로 클로저를 전달할 수 있다.
- 함수의 반환 타입으로 클로저를 사용할 수 있다.
스위프트에서 클로저 표현은 최적화되어 간결하고 명확하다.
- 문맥에서 인자 타입과 반환 타입의 추론
- 축약된 인자 이름
- 후위 클로저 문법
예시
정렬 메소드 - sorted(by:) 메서드. 배열 값을 정렬하는 메서드.
인라인 클로저란? → 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
// 타입 추론 -> 타입 생략
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
// 인자 이름 축약 -> 인자 입력 받는 부분 생략
reversedNames = names.sorted(by: { $0 > $1 } )
후위 클로저란? → 함수의 마지막 인자가 클로저로, 인자 형태로 클로저를 보내는 것이 아닌 함수의 가장 마지막에 클로저를 꼬리처럼 덧붙여서 쓰는 것이다.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
->
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
// 만약 함수의 마지막 인자가 클로저이면 후위 클로저로 괄호 생략
reversedNames = names.sorted { $0 > $1 }
doSomething () { () -> () in
print("Hello!")
}
클로저의 값 캡쳐 (Capturing Values)
클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 원본 값이 사라져도 클로저가 내부적으로 저장하고 있어 클로저의 바디 안에서 그 값을 활용할 수 있다.
클로저는 값, 참조 타입에 관계 없이 참조 캡쳐한다.
만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고 그 클로저가 그 인스턴스를 캡쳐링하면 강한 순환 참조에 빠지게 된다. 인스턴스는 클로저를 참조하고, 클로저는 인스턴스를 참조하여 서로가 서로를 참조하고 있어서 메모리에서 둘 다 해제되지 않는 강한 순환 참조가 발생한다. 즉, 인스턴스의 사용이 끝나도 메모리를 해제하지 못한다.
이를 위해 캡쳐 리스트를 사용한다. 클로저 캡쳐 리스트를 이용해 weak, unowned로 캡쳐한다.
탈출 클로저란? (Escaping Closures)
클로저를 함수의 파라미터로 넣을 수 있는데 함수 밖(함수가 끝나고)에서 실행되는 클로저 예를 들어, 비동기로 실행되거나 completionHandler로 사용되는 클로저는 파라미터 타입 앞에 @escaping 이라는 키워드를 명시해야 한다. 키워드 escaping을 사용하는 클로저는 self를 명시적으로 언급해야 한다.
non-escaping closure란?
함수 내부에서 직접 실행하기 위해서만 사용한다. 함수의 실행 흐름을 탈출하지 않아, 함수가 종료되기 전에 무조건 실행되어야 한다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
// self 명시
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure() // 함수 안에서 끝나는 클로저
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야 합니다.
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
메모리의 스택과 힙은 무슨 역할을 하는가?
프로그램이 실행되기 위해서는 먼저 프로그램이 메모리에 로드되어야 한다. 또한 프로그램에서 사용되는 변수들을 저장할 메모리도 필요하다. 프로그램이 운영체제로부터 할당받는 메모리 공간에는 4가지로 코드, 데이터, 스택, 힙 영역이 있다.
코드 영역은 실행할 프로그램의 코드가 저장되는 영역으로, CPU가 코드 영역에 저장된 명령어를 하나씩 가져가 처리한다.
데이터 영역은 전역 변수와 정적 변수가 저장되는 영역이다. 프로그램이 종료되면 소멸한다.
힙 영역은 사용자가 직접 관리할 수 있는, 해야하는 메모리 영역이다. 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다.
스택 영역은 함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역이다. 스택 영역은 함수의 호출과 함께 할당되고, 함수의 호출이 완료되면 소멸한다.
힙 vs 스택
스택은 매우 빠르게 접근할 수 있다. 메모리 크기에 제한이 있다. 변수를 할당 해제할 필요가 없다.
힙은 메모리 크기에 제한이 없지만 스택에 비해 상대적으로 느린 액세스 속도를 가졌다.
효율적인 공간 사용을 보장하지 못하면 메모리 블록이 할당된 후 시간이 지남에 따라 메모리가 조각화되어 해제될 수 있다. 메모리를 관리해야 한다. (변수를 할당하고 해제하는 관리를 해야한다.)
클로저는 스택인가 힙인가? 이스케이핑 클로저는?
클로저는 참조타입이다.
함수와 클로저는 참조 타입이다. 함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조가 할당된다. 그래서 만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조하고 있다. 함수 포인터를 저장한다고 생각하면 된다.
왜 non-escaping과 escaping을 나눴을까?
논이스케이핑 클로저가 함수 내부에서만 쓰이므로 컴파일러가 메모리 관리를 지저분하게 하지 않아도 돼서 성능이 향상되기 때문이다.
논이스케이핑의 경우 함수가 종료됨과 동시에 클로저도 사용이 끝나지만,
이스케이핑의 경우 함수가 종료되더라도 실제 클로저가 사용되지 않을 때까지 메모리를 추적해야한다.
이스케이핑 클로저의 경우 함수의 호출이 완료되었을 때 소멸되서는 안 되고 함수가 끝나고도 실행되어야 하므로 함수의 종료와 함께 소멸되는 스택 영역이 아닌 힙 영역에 할당된다.
논이스케이핑 클로저의 경우 함수가 종료되기 전에 실행이 완료되므로 스택에 할당된다.
참고
'Swift > 스위프트 정리' 카테고리의 다른 글
[Swift] Concurrency - 2 (1) | 2022.11.12 |
---|---|
[Swift] Concurrency - 1 (0) | 2022.10.30 |
[Swift] Swift 메모리 관리 - ARC란? (0) | 2022.05.18 |
[Swift] Dynamic Dispatch란? (0) | 2022.04.14 |
[Swift] Delegate와 Retain 여부! (0) | 2022.03.15 |