Swift에서 클로저(Closure)는 독립적인 코드 블록으로, 함수와 유사하게 사용할 수 있다.
하지만 함수와 달리 이름이 없으며, 다른 변수나 함수의 인자로 전달할 수도 있다.
클로저는 강력한 기능을 제공하지만, 메모리 관리 문제(특히 강한 참조 순환 문제)를 유발할 수 있다.
이번 글에서는 클로저의 기본 개념부터 메모리 관리까지 깊이 있게 살펴본다.
클로저는 함수와 비슷한 방식으로 작성되지만, 더 간결한 문법을 제공한다. 기본적인 클로저의 형태는 다음과 같다.
{ (매개변수) -> 반환 타입 in
실행 코드
}
예제를 통해 살펴보자.
let greet = { (name: String) -> String in
return "Hello, \(name)!"
}
print(greet("Swift")) // "Hello, Swift!"
위 코드에서 greet
변수는 클로저를 저장하고 있으며, 이를 호출하면 함수처럼 동작한다.
Swift는 클로저의 문법을 최적화하여 더 간결하게 작성할 수 있도록 지원한다.
Swift는 타입 추론을 제공하므로, 컴파일러가 타입을 유추할 수 있다면 생략할 수 있다.
let greet = { name in
"Hello, \(name)!"
}
print(greet("Swift"))
$0
, $1
과 같은 단축 인자 사용단일 표현식인 경우 return
을 생략할 수 있으며, 매개변수를 $0
, $1
등의 단축 인자로 사용할 수도 있다.
let multiply = { $0 * $1 }
print(multiply(3, 4)) // 12
함수의 마지막 매개변수로 클로저를 전달하는 경우, 후행 클로저(Trailing Closure) 문법을 사용할 수 있다.
func perform(action: () -> Void) {
action()
}
// 일반적인 클로저 전달 방식
perform(action: {
print("클로저 실행")
})
// 후행 클로저 사용
perform {
print("후행 클로저 실행")
}
클로저는 실행될 때 외부 변수나 상수의 값을 캡처할 수 있다. 이를 클로저의 캡처(Capturing Values)라고 한다.
func makeIncrementer(amount: Int) -> () -> Int {
var total = 0
return {
total += amount
return total
}
}
let incrementByTwo = makeIncrementer(amount: 2)
print(incrementByTwo()) // 2
print(incrementByTwo()) // 4
print(incrementByTwo()) // 6
위 코드에서 total
변수는 makeIncrementer
함수가 끝난 후에도 계속 유지된다. 이는 클로저가 total
을 캡처하고 있기 때문이다.
Swift는 자동 참조 카운트(ARC, Automatic Reference Counting)를 사용하여 메모리를 관리한다. 하지만 클로저를 사용할 때, 강한 참조 순환(Strong Reference Cycle)이 발생할 수 있다.
클로저가 클래스 인스턴스를 캡처할 경우, 강한 참조가 유지되어 메모리 누수(Memory Leak)가 발생할 수 있다.
class Person {
var name: String
var greeting: (() -> Void)?
init(name: String) {
self.name = name
}
func setGreeting() {
greeting = {
print("Hello, my name is \(self.name)")
}
}
}
var person: Person? = Person(name: "Alice")
person?.setGreeting()
person = nil // 메모리 해제되지 않음 (메모리 누수 발생)
여기서 self.name
을 클로저가 캡처하면서 self
가 강하게 참조된다. 따라서 person = nil
을 실행해도 메모리가 해제되지 않는다.
[weak self]
와 [unowned self]
를 활용한 해결이 문제를 해결하려면 weak
또는 unowned
를 사용하여 약한 참조(Weak Reference)로 캡처해야 한다.
class Person {
var name: String
var greeting: (() -> Void)?
init(name: String) {
self.name = name
}
func setGreeting() {
greeting = { [weak self] in
guard let self = self else { return }
print("Hello, my name is \(self.name)")
}
}
}
var person: Person? = Person(name: "Alice")
person?.setGreeting()
person = nil // 메모리 정상 해제
[weak self]
vs [unowned self]
[weak self]
: 선택적(optional) 참조를 사용하여, 인스턴스가 해제되면 nil
이 됨.[unowned self]
: 비선택적(non-optional) 참조를 사용하며, 인스턴스가 해제되었는데 접근하면 런타임 오류 발생.일반적으로 [weak self]
가 더 안전한 선택이다.
클로저는 네트워크 요청, 애니메이션, GCD(Grand Central Dispatch)에서 비동기 처리를 수행하는 데 자주 사용된다.
func fetchData(completion: @escaping (String) -> Void) {
DispatchQueue.global().async {
sleep(2) // 네트워크 요청 시뮬레이션
let data = "데이터 로딩 완료"
DispatchQueue.main.async {
completion(data)
}
}
}
fetchData { result in
print(result) // "데이터 로딩 완료"
}
SwiftUI에서는 뷰 업데이트를 위한 핸들러로 클로저를 자주 활용한다.
struct ContentView: View {
@State private var text = "Hello"
var body: some View {
VStack {
Text(text)
Button("변경") {
text = "클로저 활용!"
}
}
}
}
Swift의 클로저는 코드 블록을 변수처럼 다룰 수 있도록 해 주며, 함수형 프로그래밍을 지원하는 강력한 기능이다. 하지만 클로저의 값 캡처로 인해 메모리 누수가 발생할 수 있으므로 [weak self]
나 [unowned self]
를 적절히 사용해야 한다.