[iOS] Xcode 메모리 누수 디버깅 Debug Memory Graph

우리 제미나이님이 만드신 메모리 누수 이미지

TIL 16일 차 - Xcode 메모리 누수 디버깅 (Memory Graph)

Xcode에서 메모리 누수를 확인하는 대표적인 방법중하나는 Debug Memory Graph이다.

 

화면을 닫았는데 ViewController가 사라지지 않을 때, 이 화면 왜 계속 살아있지? 싶을 때 종종 사용한다.

 

[STEP-0: Memory Graph 진입하기]

0. Malloc Stack Logging 설정 키기

 

먼저 0단계로 Malloc Stack Logging 설정을 켜는 것을 추천한다. 이 설정이 꺼져 있어도 메모리 그래프는 볼 수 있지만, 켜두어야만 해당 메모리가 '코드의 몇 번째 줄'에서 생성되었는지 정확한 위치(Backtrace)를 추적할 수 있다.

 

 

[0. Malloc stack Logging 설정 방법]

1. 상단 Run ▶︎ 버튼 옆에 위치한 앱 이름 클릭

2. Edit Scheme... 선택

단축키: ⌘ + <

 

3. 왼쪽 사이드 바 'Run' -> 상단 탭 'Diagnostics' 선택

4. Malloc Stack Logging 체크하기

5. close 클릭

>> Tip: 메모리 문제를 찾을 때가 아니면 빌드 성능에 영향이 있으므로 평소엔 옵션을 꺼두는 게 좋다.

 

 

[STEP-1: 누수 발생 후 Memory Graph 진입하기]

1. 앱 다시 실행 (단축키: ⌘ + R)

2. 문제가 되는 화면에 진입하기

3. 뒤로가기 (화면 닫기)

4. Xcode 하단 디버그 바에서 Memory Graph 버튼 클릭 (동그라미 세 개가 연결된 아이콘)

동그라미 세개 연결된 모양 클릭



[STEP-2: 메모리 그래프 분석하기]

0. Debug Memory Graph 진입하기

1. 왼쪽 페이지 하단의 문서 모양 아이콘 클릭

2. Debug Navigator에서 남아있는 ViewController 찾기

3. 그래프에서 참조되어 누수가 발생하고 있는 범인 찾기

 

 

1. 왼쪽 페이지 하단 필터바의 'Show only content from workspace' 버튼(문서 모양 아이콘)을 클릭한다.

하단의 문서 모양 클릭

이렇게 하면 시스템 라이브러리 객체는 숨겨지고, 내가 짠 코드의 객체만 남아서 개발자가 누수를 찾기 훨씬 편해진다.

 

2. 왼쪽의 리스트(Debug Navigator)에서 사라졌어야 하는데 남아있는 ViewController가 있는지 확인한다.

총 2개

 

 

가운데 그래프에서 참조되고 있는 뷰 컨트롤러의 왼쪽 상단에 있는 (...) 점 세 개 버튼을 누르고 모두 체크를 해준다.

3. Closure가 굵은 선으로 뷰 컨트롤러를 잡고 있는(Capture) 모습을 확인할 수 있다.

Swift의 클로저는 기본적으로 내부에서 사용하는 외부 변수를 Strong(강한 참조)으로 캡처하므로 self를 쓰면 self를 storng으로 참조하게 된다.

 

이때 보이는 창의 On shortest path to root항목의 의미는

  • Yes -> 앱 생명주기 루트(UIWindow등)부터 정상적으로 연결되어 살아있는 객체
  • No -> 루트와의 연결은 끊어졌지만, 자기들끼리 서로 잡고 있어 메모리에 떠다니는 상태 (즉, 메모리 누수)

그래프에 [capture] 노드가 2개 보인다는 의미는 self를 캡처하는 클로저가 최소 2개로 [weak self]를 써주어서 누수를 고칠 수 있다.

 

 

이제 이 누수를 추적해서 디버깅을 해보자

 

[STEP-3: 코드 수정 및 누수 해결]

1. 그래프에서 누수로 추정되는 클로저의 [capture]블록을 선택한다.

2. 우측의 Inspector 패널을 연다.

3. Inspector 영역에서 Backtrace(Allocation Call Stack) 영역을 확인한다.

4. 하얀색 글씨로 활성화된 코드 라인 우측에 표시된 작은 화살표 버튼을 클릭한다.

 

5. 문제가 되는 코드로 진입하고 수정한다.

누수가 발생중인 코드 (수정 전)

범인: self.fetchImages()

 

1. ViewController -> Button (소유)

  • ViewController(self)는 navigationItem을 가지고 있고, 그 안의 rightBarButtonItem을 소유함
  • (self가 버튼을 강하게 잡고 있음)

2. Button -> Closure (소유)

  • UIBarButtonItem은 버튼이 눌렸을 때 실행할 행동(primaryAction의 클로저)을 메모리에 가지고 있어야 함
  • (버튼이 클로저를 강하게 잡고 있음)

3. Closure -> ViewController (소유)

  • 클로저 {...} 안에서 self.fetchImages()를 호출함
  • 클로저는 실행될 때 self가 메모리에 있어야 하므로, 기본적으로 self를 강하게 붙잡음
  • (클로저가 self=뷰컨트롤러를 강하게 잡고 있음)

 

결과: self -> Button -> Closure -> self.... 무한 루프 (메모리 누수 발생)

 

이를 해결하기 위해 클로저가 self를 약하게(Weak)참조하도록 캡처리스트 [weak self]를 사용해준다.

weak를 붙이는 순간 self가 메모리에서 사라져서 nil이 될 수도 있다는 뜻이 되므로 self가 옵셔널로 변하기 때문에 self?로 변경된다.

누수를 해결한 코드 (수정 후)

이렇게 하면 고리가 끊어져 ViewController가 정상적으로 deinit이 된다.

 

이와 같이 누수가 일어나지 않도록 코드를 수정하고 앱을 재실행해주자

정상적으로 누수가 처리되었다면 아까와 동일하게 누수를 발생하도록 화면을 들어갔다 나가고 Memory Graph 다시 열었을 때 [capture] 노드가 사라져야 한다.

 

누수를 하나 해치웠더니 2개였던 누수가 1개로 줄었다.

 

같은 방식으로 Backtrace에서 남은 누수 코드들에 접근해 보자

누수가 발생중인 코드 (수정 전)

이번 누수는 함정처럼 숨어있어 찾기가 참 힘들다.

DispatchQueue에 [weak self]를 써주었기 때문에 누수가 없어야 할 것처럼 보이지만 그 바깥쪽 클로저에서 누수가 콸콸 터지고 있다.

 

1. 숨겨진 self: Swift에서는 클래스 안에서 images = items라고만 적어도, 컴파일러는 이걸 self.images = items이라고 해석한다

2. 강한 참조 발생: repository.fetchImages 뒤에 붙은 클로저 { result in...} 안에서 self.images에 접근하고 있기 때문에, 이 클로저는 self를 강하게 붙잡는다. (Strong Capture)

누수를 해결한 코드 (수정 후)

문제가 되는 바깥쪽 클로저도 [weak self]를 써주고 guard let self else { return }으로 self가 없으면(화면이 닫혔으면) 아래의 로직이 아예 실행하지 않도록 효율적으로 만들어준다.

 

 

이렇게 남은 누수도 Backtrace를 통해 추적하여 해결해 주니, 최종적으로 ImageViewController가 리스트에서 깔끔하게 사라졌다.

 

'iOS ' 카테고리의 다른 글

[iOS] Swift Playground를 이용해 실습하기  (0) 2025.12.23
[iOS] Swift와 Xcode, iOS 생태계 이해하기  (0) 2025.12.23