질문의 배경:
클로저는 함수형 언어에서 기원한 개념으로, 코드 블록을 값으로 전달할 수 있게 해줍니다. 스위프트에서는 클로저(다른 언어의 람다와 유사)는 처음부터 존재했습니다. 이를 통해 비동기 호출을 우아하게 구현하고, 액션을 위임하며, 컬렉션을 정렬하고 필터링하는 등의 작업을 수행할 수 있습니다.
문제:
클로저는 주변 컨텍스트에서 변수를 캡처합니다. 이러한 변수 중에 클래스의 객체에 대한 참조가 있을 경우, 특히 클로저가 이 클래스의 속성으로 저장되어 있을 때 retain cycle이 발생할 수 있습니다. 또한 escaping과 non-escaping 클로저의 차이를 이해하고 값을 캡처하는 방식이 어떻게 작동하는지를 인식하는 것이 중요합니다.
해결책:
스위프트는 클로저를 독립적인 객체로 구현하였으며, 이들은 컨텍스트를 캡처할 수 있습니다. 클로저의 소유자는 아키텍처에 주의해야 합니다.
코드 예시:
class Performer { var onComplete: (() -> Void)? func doWork() { onComplete = { print("작업 완료!") } } } // 클로저 내부에서 self 캡처 class Test { var value = 0 func start() { DispatchQueue.global().async { [weak self] in self?.value = 1 } } }
주요 특징:
클로저가 self를 명시적으로 캡처하지 않을 경우, retain cycle이 발생할 수 있나요?
네, 클로저가 클래스의 strong property로 저장되고, 클로저 내부에서 self에 접근할 경우, 명시적으로 self를 쓰지 않아도 retain cycle이 발생합니다. 가능하면 [weak self]/[unowned self]를 사용하거나 클로저를 로컬로 만드는 것이 좋습니다.
escaping 클로저와 non-escaping 클로저의 차이는 무엇인가요?
Escaping 클로저는 함수 실행이 끝난 후 호출될 수 있으며, 일반적으로 비동기 작업에 사용됩니다. Non-escaping 클로저는 함수가 실행되는 동안 호출됩니다. Escaping 클로저의 self 캡처 순서는 다릅니다.
코드 예시:
func asyncWork(completion: @escaping () -> Void) { DispatchQueue.global().async { completion() } }
클로저가 캡처한 변수의 값을 변경할 수 있나요?
네, 클로저가 var로 선언된 변수를 캡처한 경우, 그 값을 변경할 수 있습니다(값 타입의 경우). 클래스의 경우 항상 참조가 되므로 속성을 변경할 수 있습니다.
코드 예시:
var value = 1 let closure = { value += 1 } closure() print(value) // 2
ViewController가 클로저를 속성으로 저장하고, 내부에서 self에 직접 접근할 때 발생합니다. 그 결과 retain cycle이 발생합니다: ViewController => 클로저 => ViewController. 화면 전환이 이루어져도 메모리가 해제되지 않습니다.
장점: 코드가 간결하고 짧아 보입니다.
단점: 메모리 누수, ViewController가 메모리에 "고착됨", 잠재적인 버그 발생 가능성.
클로저 내부에서 [weak self] 또는 [unowned self]를 사용하거나 클로저가 객체의 생애 주기보다 오래 유지되지 않도록 합니다. 코드 리뷰에서 이러한 부분을 검토합니다.
장점: 자원 해제가 올바르며, 예기치 않은 메모리 누수가 없습니다.
단점: [weak self]는 해제 시 주의가 필요하며, 잘못된 사용 시 암묵적인 크래시가 발생할 수 있습니다.