iOS Development Guide

클로저와 self에서 순환 참조가 생기는 이유

핵심은 "누가 클로저를 소유하는가"다. 객체가 클로저를 프로퍼티로 strong 소유하고, 그 클로저가 다시 self를 strong 캡처하면 순환 참조가 생긴다.

Closure capture semantics Ownership graph based Knowledge item #14
학습 날짜

기록 없음

문제가 터지는 대표 코드

final class LoginViewModel {
    var onSuccess: (() -> Void)?

    func bind() {
        onSuccess = {
            self.routeToHome() // self strong capture
        }
    }

    func routeToHome() {}
}
`self`가 `onSuccess` 클로저를 소유하고, 클로저가 `self`를 strong으로 캡처해 원형 참조가 된다.

왜 ARC가 못 푸는가

  • ARC는 참조 수를 계산할 뿐 "관계의 의도"를 모른다.
  • 서로 strong이면 참조 수가 0이 안 된다.
  • 결국 설계에서 소유권 방향을 끊어줘야 한다.

해결 기본 패턴

onSuccess = { [weak self] in
    guard let self else { return }
    self.routeToHome()
}

수명 보장이 강하면 `[unowned self]`도 가능하지만, 전제가 깨지면 크래시 위험이 있다.

캡처 리스트 오해 정리

  • [self]는 strong 캡처를 명시하는 것이라 순환 참조를 해결하지 못한다.
  • [weak self]는 optional self가 되므로 nil 분기 처리가 필요하다.
  • [unowned self]는 nil 불가 전제를 두므로 안전성 검토가 선행되어야 한다.

실무 체크리스트

  • 객체 프로퍼티에 저장되는 escaping 클로저인지 확인
  • 클로저 내부 self 사용 여부 검색
  • deinit 로그/메모리 그래프로 해제 확인