Memory Graph: 객체 그래프와 순환 참조 감지

저자: IT Sectr 게시일: 2026-05-07 읽는 시간: 10 분

Memory Graph는 Xcode Debug Navigator의 시각적 도구로, 애플리케이션 RAM에 있는 객체들의 그래프와 상호 참조를 표시합니다. 힙 덤프와 달리, Memory Graph는 단순한 객체 목록이 아닌, 각 노드가 객체이고 각 간선이 참조(strong, weak, unowned)인 방향성 참조 그래프를 보여줍니다. Apple WWDC 2018에 따르면, 이 도구는 힙 덤프의 원시 데이터를 분석할 필요 없이 몇 초 만에 retain cycle과 메모리 누수를 시각적으로 감지할 수 있습니다.

핵심 요약

  • Memory Graph는 Xcode의 메모리 내 객체 그래프로, 객체 간의 참조를 실시간으로 보여줍니다.
  • Retain cycle은 그래프에서 닫힌 루프로 감지됩니다 — 두 개 이상의 객체가 서로 강한 참조로 연결되어 있습니다.
  • Backtrace는 그래프의 각 간선에 대해 참조가 설정된 위치와 시간을 보여주어 누수 원인을 쉽게 찾을 수 있게 합니다.
  • 필터링은 클래스 이름과 참조 유형(strong/weak)으로 문제 객체를 빠르게 격리할 수 있습니다.
  • 통합된 Xcode Memory Report로 메모리 사용량 변화를 실시간으로 추적할 수 있습니다.

Memory Graph란 무엇이며 어떻게 작동하나요

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는 연결의 시각적 그림을 제공하여 순환 참조 탐색에 최적화되어 있습니다. 두 도구는 서로 보완적입니다: 먼저 Memory Graph로 retain cycle을 빠르게 감지한 다음, Instruments Allocations를 통해 힙 덤프로 retained size를 정확히 측정합니다. objc.io의 경험에 따르면, 두 방법의 조합은 메모리 누수 시나리오의 95%를 커버합니다.

Memory Graph로 retain cycle 감지하기

Retain cycle은 두 개 이상의 객체가 서로를 강한 참조로 유지하여 닫힌 루프를 형성하는 상황입니다. ARC는 각 객체의 retain count가 0에 도달하지 않기 때문에 이러한 루프를 해제할 수 없습니다. 전형적인 예: ViewController와 View에서 View가 self(ViewController)를 캡처하는 클로저에 대한 strong reference를 가진 경우입니다. Memory Graph는 이러한 루프를 링(사이클) 형태로 표시하여 빠르게 식별할 수 있게 강조합니다.

Xcode가 retain cycle을 감지하면 주황색 윤곽선으로 강조하고 Debug Navigator에 경고를 표시합니다. 사이클을 클릭하면 닫힌 루프를 형성하는 참조 체인을 볼 수 있습니다. 개발자는 강한 간선 중 어떤 것이 약해져야 하는지 결정하면 됩니다 — 일반적으로 하위 객체에서 상위 객체로의 참조(예: delegate 또는 closure)입니다.

swift
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이 사라집니다.

swift
// 수정된 코드 — 약한 self 캡처
service.fetchData { [weak self] data in
    guard let self else { return }
    self.updateUI(data)
}

Xcode의 Memory Graph Debugger 인터페이스

인터페이스 Memory Graph Debugger는 세 개의 패널로 구성됩니다: 왼쪽 — 모든 활성 객체 목록(클래스별로 그룹화)과 인스턴스 수; 중앙 — 드래그 가능한 노드가 있는 시각적 그래프; 오른쪽 — 선택한 객체 또는 간선의 인스펙터입니다. 객체 목록에는 클래스 아이콘, 메모리의 인스턴스 수, 총 retained size 및 전체 힙에서의 비율이 표시됩니다. 클래스 이름으로 필터링할 때 정규 표현식을 지원합니다.

그래프 탐색

그래프 노드는 가독성을 높이기 위해 드래그할 수 있습니다. 노드를 더블 클릭하면 객체에 대한 상세 정보가 열립니다: 모든 속성(유형 및 값 포함), 각 속성에 대한 호출 스택(backtrace), 그리고 retain/release 기록이 표시됩니다. Backtrace는 핵심 기능입니다: 객체에 대한 참조를 설정한 정확한 코드 줄을 보여줍니다. 이를 통해 전체 코드를 수동으로 검토하지 않고도 누수 원인을 찾을 수 있습니다.

복잡한 그래프의 경우 Xcode는 Layout → Hierarchical(계층형) 또는 Cluster(클러스터형)를 통해 자동 레이아웃을 제공합니다. 계층형 레이아웃은 루트 객체를 위쪽에, 하위 객체를 아래쪽에 배치하여 체인 검색을 단순화합니다. 클러스터형은 관련 객체를 클러스터로 그룹화하여 그래프에 여러 개의 독립된 그룹이 있을 때 유용합니다. Apple에 따르면, 대부분의 앱에서는 계층형 레이아웃이 권장됩니다 — 직관적이고 시각적 분석 시간이 덜 소요됩니다.

lldb
// 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 옵션을 사용하여 순환 참조에 포함된 노드만 표시합니다.

Backtrace를 사용한 원인 찾기

retain cycle이 발견되면 사이클의 간선을 클릭하고 인스펙터 패널을 엽니다. Backtrace 섹션에는 이 참조가 설정된 시점의 호출 스택이 표시됩니다. 예를 들어, 간선이 closure에서 self로 연결되면 backtrace는 closure가 생성된 메서드와 코드 줄을 보여줍니다. 이를 통해 추측할 필요가 없어집니다 — 문제가 되는 참조의 생성 지점을 즉시 확인할 수 있습니다. WWDC Labs에 따르면, backtrace 분석은 retain cycle 진단 시간을 15~20분에서 2~3분으로 단축합니다.

swift
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 사용 실전 팁

숙련된 개발자는 Memory Graph를 누수 탐색뿐만 아니라 사전 예방적 메모리 관리를 위해 사용합니다. 새로운 델리게이트, 클로저 또는 NotificationCenter 구독을 추가하는 등 주요 아키텍처 변경 후 Memory Graph를 확인하세요. 일반적인 시나리오를 실행하고 객체가 올바르게 해제되는지, retain cycle이 없는지 확인하면 됩니다. 이 작업은 2~3분이 소요되지만 이후 수 시간의 디버깅을 방지합니다.

Memory Report와의 조합

Memory Report는 Xcode(Debug Navigator 탭)에서 실시간 메모리 사용량 그래프를 보여줍니다. Memory Graph와 함께 사용하세요: 메모리 사용량이 급증할 때 Memory Graph를 엽니다. 예를 들어, 이미지를 로드하는 셀이 있는 긴 목록을 스크롤할 때 Memory Graph는 어떤 객체가 생성되고 해제되는지 보여줍니다. 객체 수가 감소 없이 증가하면 crash로 이어지기 전에 확인할 수 있는 잠재적 누수입니다. Apple에 따르면, Memory Graph + Memory Report 조합은 Xcode 12 이후 모든 iOS 개발자에게 권장되는 워크플로입니다.

objective-c
// 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에서만 작동하나요, 아니면 Swift에서도 작동하나요?

Memory Graph는 Objective-C runtime을 사용하므로 두 언어 모두에서 작동합니다. ObjC와 호환되는 Swift 객체(NSObject를 상속받거나 @objc가 표시된 객체)는 완전히 표시됩니다. ObjC 브리징이 없는 순수 Swift 구조체와 클래스는 제한적으로 표시됩니다.

Memory Graph가 일부 객체를 표시하지 않는 이유는 무엇인가요?

객체는 Objective-C runtime에 등록되어야 합니다. Swift value types(struct, enum)는 표시되지 않습니다. 클래스가 NSObject를 상속받거나 Memory Graph에서 표시되도록 @objc 속성을 사용하는지 확인하세요.

그래프에서 간선 색상을 어떻게 해석하나요?

파란색은 strong reference로, 객체를 유지합니다. 녹색은 weak reference로, 수명 주기에 영향을 주지 않습니다. 회색은 unowned reference입니다. Retain cycle은 파란색 간선으로만 형성됩니다.

Memory Graph가 앱을 느리게 하나요?

그래프 구축은 앱을 1~3초 동안 일시 중지하고 Xcode의 메모리 사용량을 200~500MB 일시적으로 증가시킬 수 있습니다. 검사가 중단점 일시 중지 중에 발생하므로 앱 자체는 느려지지 않습니다.

Memory Graph를 분석을 위해 내보낼 수 있나요?

Xcode는 그래프를 직접 내보내는 것을 지원하지 않습니다. 문서화를 위해 스크린샷을 사용하거나 lldb 스크립트 heap.find_variable를 사용하여 프로그래밍 방식으로 데이터를 추출하세요. 자세한 분석은 힙 덤프와 함께 Instruments Allocations를 사용하세요.

요약

  • Memory Graph는 Xcode의 시각적 도구로, 메모리의 객체 그래프와 해당 참조를 표시합니다.
  • Retain cycle은 파란색(strong) 간선의 닫힌 루프로 표시됩니다 — Xcode가 주황색으로 강조합니다.
  • Backtrace는 그래프의 각 간선에 대해 문제가 되는 참조가 생성된 정확한 코드 위치를 보여줍니다.
  • 필터링은 클래스와 참조 유형별로 수천 개의 객체가 있는 그래프에서 누수를 격리할 수 있게 합니다.
  • 클로저는 Swift에서 retain cycle의 주요 원인이며, Memory Graph는 이를 {} 노드로 표시합니다.
  • Weak 및 unowned는 사이클을 끊는 해결책이지만, nil 안전성 때문에 weak가 더 선호됩니다.
  • 아키텍처 변경 후 정기적인 Memory Graph 확인은 프로젝트의 메모리 회귀를 방지합니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기