on
Swift Closure의 Capture
1. 개요
Swift에서 Closure는 코드 블록을 변수처럼 다룰 수 있는 기능입니다. 함수(Function)와 유사하지만, 주변 컨텍스트(Context)를 캡처할 수 있다는 점이 Closure의 가장 큰 특징이에요.
이 글에서는 Swift Closure의 Capture 방식에 대해 정리해 보려 합니다.
마지막에는 weak self
외에도 필요한 프로퍼티만 캡처해서 순환 참조를 방지하는 방법까지 예시와 함께 설명드릴게요.
2. Swift에서 Closure의 Capture 방식
클로저는 정의된 컨텍스트(Context)를 기억해서, 사용 시점에서도 외부 변수나 객체에 접근할 수 있어요. 이를 ’캡처(Capture)’라고 불러요.
Swift의 Capture 방식은 다음 세 가지로 나뉘어요.
- Strong Capture (기본값): 변수나 객체를 강하게 참조(Strong Reference)해요. 참조 카운트가 증가하기 때문에 순환 참조가 발생할 수 있어요.
- Weak Capture: 참조 카운트를 올리지 않고 캡처해요. 캡처한 객체가 해제되면 nil로 바뀌어요. (Optional 타입)
- Unowned Capture: 참조 카운트를 올리지 않고 캡처해요. 객체가 해제되면 접근 시 크래시가 나요. (Non-Optional)
또한, 값 타입(Value Type)과 참조 타입(Reference Type)의 캡처 방식에도 중요한 차이가 있어요.
- 값 타입(Value Type): 클로저가 캡처하는 시점의 값을 복사해요. 이후 원본 값이 변경되더라도 클로저 내부에선 캡처 시점의 값이 유지돼요.
- 참조 타입(Reference Type): 클로저가 참조를 잡고 있어서, 외부 객체의 변경 사항이 클로저 내부에서도 반영돼요.
3. 순환 참조와 [weak self]
iOS 개발에서 순환 참조는 흔히 발생하는 메모리 이슈예요. 예를 들어 ViewController가 자신 안의 클로저를 Strong으로 캡처하고, 그 클로저가 ViewController를 프로퍼티로 들고 있는 경우 서로가 서로를 강하게 참조하고 있어서 메모리에서 해제되지 않는 문제가 발생해요.
이런 상황에서 우리는 클로저 안에서 [weak self]
를 사용해 순환 참조를 방지해요. weak self를 사용하면 클로저가 self를 참조할 때 참조 카운트를 올리지 않기 때문에, ViewController가 해제될 수 있게 돼요.
그리고 대부분의 경우, weak self는 안전하고 효과적인 선택이에요. 특히 네트워크 요청, 비동기 작업 등 self가 사라질 수 있는 상황에서는 weak self를 쓰는 것이 기본이 돼요.
4. 캡처 리스트를 활용한 순환 참조 방지
순환 참조를 방지하는 대표적인 방법은 [weak self]를 사용하는 것이에요. 하지만 때로는 self 전체를 캡처할 필요 없이, 필요한 프로퍼티만 선택적으로 캡처하는 것이 더 명확하고 안전할 때도 있어요.
예를 들어 title 프로퍼티만 필요하다면 다음과 같이 작성할 수 있어요.
viewModel.load { [title = self.title] in
print("Loaded title: \(title)")
}
이렇게 하면 self 전체를 캡처하지 않고, title 값만 클로저에 복사해두는 방식으로 순환 참조를 피할 수 있어요.
이 방법은 값 타입(Value Type)일 때는 값을 복사하는 효과가 있고, 참조 타입(Reference Type)일 때는 self 전체가 아닌 필요한 프로퍼티만 캡처하기 때문에 순환 참조를 끊는 데에도 유용해요. 다만, 참조 타입의 경우 캡처하는 프로퍼티가 다른 객체를 또 강하게 참조하고 있다면 그 부분까지 고려해야 해요.
5. 캡처 리스트로 객체 생명주기 설계하기
클로저의 캡처 리스트는 단순히 순환 참조를 방지하는 것 외에도, 객체의 생명주기(Lifecycle)를 설계하는 용도로 사용할 수 있어요.
예를 들어 아래와 같이 [weak self]
를 사용하면 self가 해제되면 클로저는 silent fail로 동작을 종료해요.
foo.handler = { [weak self] in
guard let self else { return }
print(self.bar.name)
}
하지만 self 전체가 아닌 특정 프로퍼티만 직접 캡처하면, 그 프로퍼티의 생명주기를 클로저(혹은 foo 객체)와 연결할 수 있어요.
foo.handler = { [bar] in
print(bar.name)
}
이 경우 foo가 해제될 때까지 bar는 메모리에 유지돼요. foo.handler가 bar를 강하게 참조하고 있기 때문이에요. 즉, 클로저의 캡처 리스트를 이용해 어떤 객체의 생명주기를 어디까지 연장할지를 명시적으로 설계할 수 있게 돼요.
이 패턴은 self는 해제되어도 괜찮지만, 특정 프로퍼티만은 살아있어야 하는 경우에 매우 유용하게 사용할 수 있어요.
6. 이것의 장점
이렇게 상황에 따라 다양한 캡처 방식을 고민하면 다음과 같은 장점이 있어요.
- 코드의 의도를 더 명확히 드러낼 수 있어요.
- 이 작업은 self가 없어도 괜찮은가? 반드시 필요한가? 코드 자체가 답을 보여줘요.
- 불필요한 guard let self 패턴을 줄여 코드가 간결해져요.
- self 전체를 캡처하지 않아도 될 때는 필요한 값만 복사/참조하는 방법으로 코드가 더 읽기 쉬워져요.