클로저(Closure)
클로저는 사용자의 코드 안에서 전달되어 사용할 수 잇는 로직을가진 중괄호"{}"로 구분된 코드 블럭이다.
쉽게 말해 클로저는 이름이 없는 함수이다. 그렇다면 함수는 이름이 있는 클로저일것이다.
클로저와 함수는 기능이 완전히 동일한데, 형태만 다르다고 생각하면 된다.
클로저를 사용하면 따로 함수를 만들어야 할 불편함 없이 즉석에서 함수를 만들 수 있다.
클로저의 형태
함수의 형태
func myFunction(매개변수 이름: 매개변수 타입) -> 반환 타입{
returun 반환타입
}
func add(x: Int, y: Int) -> Int{
return(x+y)
}
print(add(x: 10, y: 20)) // 30
클로저의 형태
{ (매개변수이름: 매개변수타입) -> 리턴타입 in
return 반환타입
}
let add = {(x: Int, y: Int) -> Int in
return(x+y)
}
print(add(10, 20)) // 30
in 키워드는 단지 중괄호가 없기 때문에 구분을 해주기 위해 사용한다
생각의 전환 ➡️ 중괄호는 클로저(함수)이다.
클로저의 타입
스위프트는 함수를 "일급객체"로 취급한다. (Swift에선 함수, 프로토콜, 클로저 모두 타입이다)
함수는 타입이다.
아래에서 함수 = 클로저 이다.
1. "함수"를 변수에 할당할 수 있다.
func aFunction1(_ param: String) -> String {
return param + "!"
}
var a: (String) -> String = aFunction1
var closure = { (param: String) -> String in
return param + "!"
}
// 인풋이 String이기 때문에 String을 입력해야한다.
print(a("clamp")) // clamp!
2. 함수를 호출할 때, "함수"를 파라미터로 전달할 수 있다. ex) completionHandler
func closureParamFunction(completion: (String) -> Void){
completion("Marco") <<-- *콜백함수
}
// 클로저 생성 후 전달
let closure = { (str: String) -> Void in
print(str)
}
closureParamFunction(completion: closure)
// 클로저 바로 전달
closureParamFunction(completion:{ str in
print(str)
})
// Marco
* 콜백함수: 함수를 실행할 때 파라미터로 전달하는 함수
3. 함수에서 함수를 반환할 수 있다.
이 3개를 모두 충족하면 함수는 타입이다. 라는 말이 성립된다.
클로저의 활용
func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void){
let c = a + b // a와 b를 더해서 c를 만들고
closure(c) // *c를 closure에 던진다. closure의 작동방식은 여기서 정하지 않는다
}
closureCaseFunction(a: 10, b: 5, closure: { (param: Int) -> () in
print(param)
print(param)
print(param)
})
closureCaseFunction(a: 12, b: 12, closure: { param -> () in
print(param + 1 + 3 + 5)
})
// 후행클로저, 파라미터가 없다면 ()도 생략할 수 있다.
closureCaseFunction(a: 10, b: 8) { param in
print(param)
}
1. 하나의 함수의 결과를 다양한 방법으로 활용이 가능하며 필요할 때 마다 새롭게 정의할 수 있다.
a와 b를 더한 결과인 c에대한 활용은 closureCaseFunction함수를 호출하는 부분에서 다양하게 만들 수 있다.
결과의 사용을 사후적으로 다양하게 정의할 수 있다는게 클로저의 장점이 될 수 있다.
사후적으로 정의가 가능하다는 부분은 활용도가 높아진다는 의미!
2. 함수의 실행이 순차적임을 보장한다.
결과적으로 a + b가 실행된 후 클로저로 전달한 함수의 실행 순서가 보장된다. *(비동기 프로그래밍)
a + b가 더해진 후 클로저로 전달되어 다음 함수가 진행될 수 있게된다.
클로저의 축약
// 함수의 정의
func performClosure(param: (String) -> Int) {
param("Swift")
}
// 문법을 최적화하는 과정
// 1) 타입 추론(Type Inference)
performClosure(param: { (str: String) in
return str.count
})
performClosure(param: { str in
return str.count
})
// 2) 한줄인 경우, 리턴을 안 적어도 됨(Implicit Return)
performClosure(param: { str in
str.count
})
// 3) 아규먼트 이름을 축약(Shorthand Argements)
performClosure(param: {
$0.count
})
// 4) 트레일링 클로저
performClosure(param: {
$0.count
})
performClosure() {
$0.count
}
performClosure { $0.count }
클로저의 메모리⭐️⭐️⭐️
클로저는 참조타입이다.
값 형식 | 참조 형식 | |
타입 | Value Type(값 형식) | Refernce Type(참조 형식) |
메모리 상의 저장 위치 |
복사시 메모리의 값이 전복사되어 전달 저장위치: Stack |
복사시 메모리의 주소를 전달 저장 위치: Heap(주소를 Stack)에 저장 |
메모리 관리 | 스택 프레임이 종료되면 메모리에서 자동 제거 | RC(Reference Counting)을 통해 메모리 관리 Swift >> ARC 기법으로 관리 |
각 형식의 타입 | tuple, struct, Int, String, Double, Enum | 클래스, 클로저 |
클로저를 생성하면 스택 영역에 클로저의 주소가 저장되며, 생성된다. 실제 클로저는 힙영역에 저장된다. 그리고 클로저는 코드영역의 실행 코드 주소를 가리키고있다.
바라보는 방향
스택에 저장된 a클로저 -> a클로저의 메모리 주소 -> a클로저가 실행할 코드영역의 코드주소
* 실제 클로저의 실행시: 스택프영역에서 클로저의 스택프레임을 만들어 코드영역의 코드들을 실행한다. 항상 함수의 직접적인 실행은 스택프레임에서 실행된다.
클로저의 캡처현상 ⭐️⭐️⭐️
var stored = 0
let closure = {(number: Int) -> Int in
stored += number
return stored
}
클로저 내부에서 외부변수인 stored의 변수에 접근하고 있다. 이 상태에서 클로저를 실행하게되면 ?
closure(3) // 3
closure(4) // 7
closure(7) // 14
stroed의 값이 누적되고 있다. 이렇게 되는 이유는 아래와 같다.
클로저 내부에서 stored 변수를 사용하고 있기 때문에 힙영역의 closure클로저는 외부의 stored 변수의 메모리 주소 공간을 갖게된다.
이 말은 값타입의 전달이 아닌 stored를 참조하게 된다는 뜻이다. 그 말은 힙 영역의 closure가 외부 변수인 stored의 주소공간을 지속적으로 갖고있다는 뜻이다.
일반적인 함수.
func calculate(number: Int) -> Int {
var sum = 0
func square(num: Int) -> Int {
sum += (num * num)
return sum
}
let result = square(num: number)
return result
}
calculate(number: 10) // 100
calculate(number: 20) // 200
calculate(number: 30) // 300
함수 내부에 함수가 존재하는 중첩함수이다. calculate(number: 10)를 실행하면 sum이란 메모리가 생기고, 내부에서 square를 실행하며 result에 담고있다. square는 sum에 10 * 10을 담게되고, return result를 하며 calculate(number: 10)가 종료된다. 종료되며 sum, result, num 모든 변수가 사라진다.
변수를 캡처하는 함수⭐️⭐️⭐️
func calculateFunc() -> ((Int) -> Int) {
var sum = 0
func square(num: Int) -> Int {
sum += (num * num)
return sum
}
return square
}
var squareFunc = calculateFunc()
squareFunc(10) // 100
squareFunc(20) // 500
squareFunc(30) // 1400
squareFunc = calculateFunc()를 실행하고 있다. calculateFunc의 리턴타입은 ((Int) -> Int) 인 클로저타입이다. 실제로 calculateFunc()는 square 함수를 리턴하고있다.
그렇다면 squareFunc에는 calculateFunc() 내부의 square(num: Int)함수가 외부의 squareFunc에 저장될 것이다.
⭐️ 그렇다면 square(num: Int) 함수만 저장될까? 내부에서 사용하는 sum 변수는?
실제로 square(num: Int) 함수(클로저)는 Heap 영역에 저장된다. squareFunc변수가 지속적으로 가리키고 있기 때문이다.
그렇다면 squareFunc가 존재하는 동안 계속 존재해야 하기 때문에 Heap영역에 존재한다.
또한 square(num: Int)함수는 외부변수인 sum을 사용한다. 그렇기 때문에 Heap 영역에 sum 변수를 지속적으로 갖고있다.
왜냐하면 square 함수는 sum 변수가 없인 어떻게 할 수 없기 때문.
클로저의 캡처현상은 이렇게 발생한다. 클로저 외부에 선언된 인스턴스도 클로저 내부에서 사용된다면 외부에 선언됐을 지라도 클로저 내부에서 가리키게된다. 그러므로 메모리에서 사라지지 않는 캡처현상이 발생한다.
변수에 할당하지 않는 경우 클로저를 가리키는 영역이 멤버가 없으므로 힙 영역에 저장이 이루어지지 않으므로 일반적인 함수와 같이 동작한다.