⎮ 메모리 누수가 발생하는 상황
이전의 순환 참조가 발생하는 ViewController를 활용해서 LeakVC를 푸시해보겠습니다
push와 pop을 반복하면 매번 새로운 LeakVC 인스턴스가 생성되지만,
기존 인스턴스들이 해제되지 못하고 메모리에 쌓이게 됩니다

⎮ Memory Graph를 확인하자

이런 상황에서 메모리 누수가 나는 것으로 의심이 된다면,
Debug Memory Graph를 열어 확인해보면 좋습니다
정상적인 상황이라면 좌측 dylib에는 LeakViewController 객체가 없어야합니다
이미 pop 되어서 사라졌기 때문이죠

그러나, 메모리 누수가 나는 상황에서는 해당 객체들이 쌓이게 됩니다

메모리 그래프를 자세히 보면, 화살표의 두께가 각각 다른데요
얇은 선이 참조가 있다고 추측하는(conservative), 굵은 선이 명확한 강한 참조입니다

위 그래프에서는 다음과 같이 해석할 수 있습니다
Closure 내부에서 LeakViewController를 강하게 참조하고 있다
실제로 코드를 봐도 동일한데,
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.tick()
}
timer가 클로저 내부에서 self를 강하게 참조하고 있음을 확인할 수 있습니다
이번에는 클로저 캡처말고, Delegate에서 weak 키워드를 사용하지 않았을 때를 확인해보겠습니다

이번에는 RealisticViewController가 NetworkManager를 잡고 있고,
다시 NetworkManager가 RealisticView를 잡고 있습니다
이를 코드로 확인해보면,
class NetworkManager {
var delegate: NetworkManagerDelegate? // weak 없음, 강한 참조
init() {
print("NetworkManager 생성")
}
deinit {
print("NetworkManager deinit - 이 메시지가 안 뜨면 메모리 누수!")
}
}
final class RealisticLeakViewController: UIViewController {
private let networkManager = NetworkManager()
{ ... }
networkManager.delegate = self
{ ... }
}
다음과 같은 구조로 되어 있기 때문에,
RealisticLeakViewController 내부에서 networkManager를 생성할 때 RC + 1
delegate 설정을 할 때 RC + 1로 총 2가 증가하게 되고,
RC가 0이 되지 않아 메모리에서 해제되지 않는 누수 상황이 발생하는 것이죠
노드를 클릭해도, 참조의 형태를 확인할 수 있습니다

⎮ 메모리 누수 디버깅 체크 리스트
결론으로 메모리 누수를 잡을 때는 다음의 플로우를 참고하면 좋을거 같다는 생각을 하게 되었습니다
1. ViewController 내부에서 deinit 로그 찍어보기
2. deinit이 안되면, 메모리 그래프 확인
3. 메모리 그래프에서 두꺼운 노드 따라가기
4. 해당 노드의 순환 참조 케이스 디버깅
메모리 누수가 발생하지 않는 코드를 작성하는 것이 가장 중요하지만,
실무에서는 작은 휴먼 에러로도 누수가 쉽게 발생할 수 있습니다.
이런 상황에서는 위와 같은 체크 플로우로 차근차근 추적해보면
누수를 빠르게 해결할 수 있습니다.
'Swift > TOPIC' 카테고리의 다른 글
| Swift | 네트워크 예외처리를 해보자(1) (0) | 2025.09.16 |
|---|---|
| Hash / Hashable / Hasher / HashTable (0) | 2025.09.09 |
| Swift | 메모리 누수(1) (feat. Instruments Leaks) (0) | 2025.08.30 |
| Swift | GCD와 Swift Concurrency (0) | 2025.08.26 |
| Swift | translatesAutoresizingMaskIntoConstraints는 왜 끄는걸까? (2) | 2025.07.20 |