Memory Graph는 Xcode Debug Navigator의 시각적 도구로, 애플리케이션 RAM에 있는 객체들의 그래프와 상호 참조를 표시합니다. 힙 덤프와 달리, Memory Graph는 단순한 객체 목록이 아닌, 각 노드가 객체이고 각 간선이 참조(strong, weak, unowned)인 방향성 참조 그래프를 보여줍니다. Apple WWDC 2018에 따르면, 이 도구는 힙 덤프의 원시 데이터를 분석할 필요 없이 몇 초 만에 retain cycle과 메모리 누수를 시각적으로 감지할 수 있습니다.
핵심 요약
Memory Graph는 Xcode Debug Navigator의 구성 요소로(Xcode 10, WWDC 2018에 도입), 디버깅 중인 프로세스의 메모리에 있는 모든 객체의 방향성 그래프를 구축합니다. 그래프의 각 노드는 클래스 인스턴스(Objective-C 또는 Swift)이고, 각 간선은 다른 객체에 대한 참조입니다. 간선 색상은 참조 유형을 나타냅니다: 파란색은 strong, 녹색은 weak, 회색은 unowned입니다. 그래프는 LLDB와 Objective-C runtime 데이터를 기반으로 구축되므로, 올바르게 작동하려면 앱이 Debug 구성으로 빌드되고 기호가 포함되어야 합니다.
작동 원리: 앱이 중단점에서 일시 중지되면 Xcode는 LLDB를 통해 runtime에 모든 활성 객체와 해당 참조를 요청합니다. LLDB는 objc_getClassList와 할당 영역 반복을 사용하여 전체 그래프를 구축합니다. ARM64(Apple Silicon)에서는 할당 추적을 위한 하드웨어 기능이 추가로 사용되어 속도 저하 없이 작동합니다. 그래프 구축 시간은 힙 크기에 따라 달라집니다: 일반적인 iOS 앱(50–200MB)의 경우 그래프는 1~3초 내에 구축됩니다.
Apple에 따르면, Memory Graph는 코드 수정이나 계측 도구 추가 없이 retain cycle을 시각화할 수 있는 유일한 도구입니다. Instruments Leaks와 달리 Memory Graph는 Xcode 내에서 실시간으로 작동하며 별도의 프로파일러를 실행할 필요가 없습니다. 따라서 개발 과정에서 메모리 누수를 빠르게 진단하는 첫 번째 선택 도구입니다.
힙 덤프는 모든 객체의 수치 표(얕은 크기, 보유 크기)를 제공하여 정량적 분석에 최적화되어 있습니다. Memory Graph는 연결의 시각적 그림을 제공하여 순환 참조 탐색에 최적화되어 있습니다. 두 도구는 서로 보완적입니다: 먼저 Memory Graph로 retain cycle을 빠르게 감지한 다음, Instruments Allocations를 통해 힙 덤프로 retained size를 정확히 측정합니다. objc.io의 경험에 따르면, 두 방법의 조합은 메모리 누수 시나리오의 95%를 커버합니다.
Retain cycle은 두 개 이상의 객체가 서로를 강한 참조로 유지하여 닫힌 루프를 형성하는 상황입니다. ARC는 각 객체의 retain count가 0에 도달하지 않기 때문에 이러한 루프를 해제할 수 없습니다. 전형적인 예: ViewController와 View에서 View가 self(ViewController)를 캡처하는 클로저에 대한 strong reference를 가진 경우입니다. Memory Graph는 이러한 루프를 링(사이클) 형태로 표시하여 빠르게 식별할 수 있게 강조합니다.
Xcode가 retain cycle을 감지하면 주황색 윤곽선으로 강조하고 Debug Navigator에 경고를 표시합니다. 사이클을 클릭하면 닫힌 루프를 형성하는 참조 체인을 볼 수 있습니다. 개발자는 강한 간선 중 어떤 것이 약해져야 하는지 결정하면 됩니다 — 일반적으로 하위 객체에서 상위 객체로의 참조(예: delegate 또는 closure)입니다.
class ViewController: UIViewController {
let service = DataService()
override func viewDidLoad() {
super.viewDidLoad()
// ❌ 순환 참조: ViewController → service → closure → ViewController
service.fetchData { self.updateUI($0) }
}
func updateUI(_ data: Data) {}
}
class DataService {
var completion: ((Data) -> Void)?
func fetchData(handler: @escaping (Data) -> Void) {
self.completion = handler
}
}
Memory Graph에 삼각형이 표시됩니다: ViewController → DataService → closure → ViewController. 해결 방법은 self를 약하게 캡처하는 것입니다: [weak self]. 수정 후 Memory Graph는 closure에서 ViewController로의 녹색 간선을 표시하고 retain cycle이 사라집니다.
// 수정된 코드 — 약한 self 캡처
service.fetchData { [weak self] data in
guard let self else { return }
self.updateUI(data)
}
인터페이스 Memory Graph Debugger는 세 개의 패널로 구성됩니다: 왼쪽 — 모든 활성 객체 목록(클래스별로 그룹화)과 인스턴스 수; 중앙 — 드래그 가능한 노드가 있는 시각적 그래프; 오른쪽 — 선택한 객체 또는 간선의 인스펙터입니다. 객체 목록에는 클래스 아이콘, 메모리의 인스턴스 수, 총 retained size 및 전체 힙에서의 비율이 표시됩니다. 클래스 이름으로 필터링할 때 정규 표현식을 지원합니다.
그래프 노드는 가독성을 높이기 위해 드래그할 수 있습니다. 노드를 더블 클릭하면 객체에 대한 상세 정보가 열립니다: 모든 속성(유형 및 값 포함), 각 속성에 대한 호출 스택(backtrace), 그리고 retain/release 기록이 표시됩니다. Backtrace는 핵심 기능입니다: 객체에 대한 참조를 설정한 정확한 코드 줄을 보여줍니다. 이를 통해 전체 코드를 수동으로 검토하지 않고도 누수 원인을 찾을 수 있습니다.
복잡한 그래프의 경우 Xcode는 Layout → Hierarchical(계층형) 또는 Cluster(클러스터형)를 통해 자동 레이아웃을 제공합니다. 계층형 레이아웃은 루트 객체를 위쪽에, 하위 객체를 아래쪽에 배치하여 체인 검색을 단순화합니다. 클러스터형은 관련 객체를 클러스터로 그룹화하여 그래프에 여러 개의 독립된 그룹이 있을 때 유용합니다. Apple에 따르면, 대부분의 앱에서는 계층형 레이아웃이 권장됩니다 — 직관적이고 시각적 분석 시간이 덜 소요됩니다.
// Memory Graph 내부에서 사용하는 LLDB 명령어
(lldb) script import lldb.macosx.heap
(lldb) script heap.find_variable("viewController")
0x600000c4b80: ViewController
(lldb) script heap.refs 0x600000c4b80
0x600000c4b80 -> 0x600003a4c00 (DataService)
ivar: _service, offset: 16
Memory Graph 분석의 체계적 접근법은 여러 단계로 구성됩니다. 1단계: 앱을 실행하고 잠재적으로 누수를 유발하는 시나리오를 수행합니다(화면 열기/닫기, 네트워크 요청 실행). 2단계: Debug Navigator에서 Memory Graph 버튼을 누릅니다 — Xcode가 그래프를 구축합니다. 3단계: 왼쪽 패널에서 주황색 retain cycle 경고를 확인합니다. 4단계: 의심스러운 객체에 대해 Show only cycles 옵션을 사용하여 순환 참조에 포함된 노드만 표시합니다.
retain cycle이 발견되면 사이클의 간선을 클릭하고 인스펙터 패널을 엽니다. Backtrace 섹션에는 이 참조가 설정된 시점의 호출 스택이 표시됩니다. 예를 들어, 간선이 closure에서 self로 연결되면 backtrace는 closure가 생성된 메서드와 코드 줄을 보여줍니다. 이를 통해 추측할 필요가 없어집니다 — 문제가 되는 참조의 생성 지점을 즉시 확인할 수 있습니다. WWDC Labs에 따르면, backtrace 분석은 retain cycle 진단 시간을 15~20분에서 2~3분으로 단축합니다.
class ProfileViewController: UIViewController {
var profileView: ProfileView!
override func viewDidLoad() {
super.viewDidLoad()
profileView = ProfileView()
// Memory Graph가 여기에 retain cycle을 표시합니다
profileView.onTap = { [unowned self] in
// ⚠️ unowned는 nil self에서 crash를 발생시킬 수 있습니다
self.navigateToDetail()
}
}
func navigateToDetail() { }
}
// ✅ 올바름: [weak self] + guard let self
profileView.onTap = { [weak self] in
guard let self else { return }
self.navigateToDetail()
}
Memory Graph는 수천 개의 객체를 표시하여 검색을 어렵게 만들 수 있습니다. 왼쪽 패널에서 필터를 사용하세요: 클래스 이름을 입력하면(예: ProfileViewController) 해당 클래스의 인스턴스만 표시됩니다. 그런 다음 해제되어야 할 인스턴스를 선택합니다(화면이 닫혔는데 객체가 남아 있는 경우). Show Reachable From을 적용하면 이 객체와 관련된 참조만 표시되고 나머지 그래프는 숨겨집니다.
숙련된 개발자는 Memory Graph를 누수 탐색뿐만 아니라 사전 예방적 메모리 관리를 위해 사용합니다. 새로운 델리게이트, 클로저 또는 NotificationCenter 구독을 추가하는 등 주요 아키텍처 변경 후 Memory Graph를 확인하세요. 일반적인 시나리오를 실행하고 객체가 올바르게 해제되는지, retain cycle이 없는지 확인하면 됩니다. 이 작업은 2~3분이 소요되지만 이후 수 시간의 디버깅을 방지합니다.
Memory Report는 Xcode(Debug Navigator 탭)에서 실시간 메모리 사용량 그래프를 보여줍니다. Memory Graph와 함께 사용하세요: 메모리 사용량이 급증할 때 Memory Graph를 엽니다. 예를 들어, 이미지를 로드하는 셀이 있는 긴 목록을 스크롤할 때 Memory Graph는 어떤 객체가 생성되고 해제되는지 보여줍니다. 객체 수가 감소 없이 증가하면 crash로 이어지기 전에 확인할 수 있는 잠재적 누수입니다. Apple에 따르면, Memory Graph + Memory Report 조합은 Xcode 12 이후 모든 iOS 개발자에게 권장되는 워크플로입니다.
// Objective-C 델리게이션을 통한 누수 예제
@interface DownloadManager : NSObject
@property (strong) id delegate; // ❌ weak이어야 합니다!
@end
@implementation DownloadManager
// Memory Graph가 retain cycle을 표시합니다:
// ViewController → DownloadManager.delegate → ViewController
@end
// 수정: weak 속성
@property (weak) id delegate;
클로저에 특별히 주의하세요 — Swift에서 retain cycle의 가장 흔한 원인입니다. 객체의 속성으로 저장된 클로저 내부에서 self를 캡처하면 고전적인 사이클이 형성됩니다. Memory Graph는 이를 캡처된 객체와 파란색 간선으로 연결된 클로저({} 기호가 있는 노드)로 표시합니다. 특히 비동기 호출, GCD, Combine 및 SwiftUI에서 사용되는 모든 클로저를 정기적으로 확인하세요. Point-Free 통계에 따르면, Swift 프로젝트의 90% 누수가 self를 캡처하는 클로저와 관련되어 있습니다.
자주 묻는 질문
Memory Graph는 Objective-C runtime을 사용하므로 두 언어 모두에서 작동합니다. ObjC와 호환되는 Swift 객체(NSObject를 상속받거나 @objc가 표시된 객체)는 완전히 표시됩니다. ObjC 브리징이 없는 순수 Swift 구조체와 클래스는 제한적으로 표시됩니다.
객체는 Objective-C runtime에 등록되어야 합니다. Swift value types(struct, enum)는 표시되지 않습니다. 클래스가 NSObject를 상속받거나 Memory Graph에서 표시되도록 @objc 속성을 사용하는지 확인하세요.
파란색은 strong reference로, 객체를 유지합니다. 녹색은 weak reference로, 수명 주기에 영향을 주지 않습니다. 회색은 unowned reference입니다. Retain cycle은 파란색 간선으로만 형성됩니다.
그래프 구축은 앱을 1~3초 동안 일시 중지하고 Xcode의 메모리 사용량을 200~500MB 일시적으로 증가시킬 수 있습니다. 검사가 중단점 일시 중지 중에 발생하므로 앱 자체는 느려지지 않습니다.
Xcode는 그래프를 직접 내보내는 것을 지원하지 않습니다. 문서화를 위해 스크린샷을 사용하거나 lldb 스크립트 heap.find_variable를 사용하여 프로그래밍 방식으로 데이터를 추출하세요. 자세한 분석은 힙 덤프와 함께 Instruments Allocations를 사용하세요.
요약
{} 노드로 표시합니다.턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.