XCode Instruments로 메모리 릭 확인하기

peppermint100
PEPPERMINT100
Published in
8 min readMar 10, 2024

서론

이전 회사에서 백엔드 개발자로 일하고 있는 평화로운 주말, 슬랙에 에러메시지가 왔다. 서버에 메모리가 부족하다는 알림. 뭐 무거운 스케쥴링이 돌아가거나 특정 요청이 많으면 한 번씩 오기는 한다.

메모리 문제가 사소하게(?) 생기면 문제 있는 인스턴스는 죽고 새로 롤링을 해서 인스턴스가 교체되도록 세팅을 해뒀기 때문에 걱정하지 않고 있었다.

그런데 이번엔 좀 심각했다.

  1. 주말 동안 한 시간 가량 서버가 다운됐던 것.
  2. 또 그 주말 앞뒤로 연차를 쓴 나는 삿포로에서 여행 중이었던 것.

자세한 내막과 해결 과정은 여기에서 확인할 수 있다.

간단히 위 글을 요약하자면,

출근 이후 부랴부랴 문제 확인에 들어갔고, 메모리가 문제였던 것으로 확인되어 살면서 처음으로 JVM 메모리 모니터링 툴을 깔아서 분석하기 시작했으며 면접 질문 대용으로 달달 외우던 JVM의 메모리 영역을 직접 확인하는 경험을 실무에 마주쳐서 했으며 결국 완벽한 해결은 하지 못했다는 것이다.

결국 메모리 문제는 중요하다! 언제 어떻게 터질지 모르니 알아두는 것이 좋다는 것이다.

회사에서도 모바일 클라이언트 개발자분들이 메모리로 고생하는 것도 봤고, 스마트폰의 제한된 메모리를 효율적으로 활용하는 것도 중요하고 일찍이 생각해왔다.

그래서 이번엔 XCode의 분석 도구인 Instrument를 사용해서 직접 진행한 프로젝트의 메모리 관련 문제를 모니터링 하는 경험을 공유해보려고 한다.

Retain Cycle

iOS 개발자들이 몸으로 체득하여 주의하고 있으며 가장 널리 알려진 ARC의 Strong Capturing으로 생기는 Retain Cycle이라는 문제가 있다.

자세한 내용은 여기를 참조하자. ARC, Retain Cycle이 뭔지는 기본적으로 알아야 한다.

Retain Cycle이 생기면 서로 참조하는 인스턴스가 해제되지 못하고 계속 남아있는 현상이 발생한다. 내 프로젝트에는 아래 코드가 그러했다.

protocol SettingSwitchTableViewCellDelegate: AnyObject {
func toggle(_ next: Bool)
}

class SettingSwtichTableViewCell: UITableViewCell {

static let identifier = "SettingSwtichTableViewCell"

var delegate: SettingSwitchTableViewCellDelegate?
...
}

// SettingSwitchTableViewCell

위 코드는 테이블 뷰의 셀 중하나로 셀 안에 UISwitch를 가지고 Delegate를 통해 동작을 바깥으로 전달해준다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let option = presenter.getSettingOption(section: indexPath.section, row: indexPath.row)

switch option {
case .switchType(let model):
guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingSwtichTableViewCell.identifier)
as? SettingSwtichTableViewCell else { return UITableViewCell() }
cell.configure(image: model.icon, backgroundColor: model.iconBackgroundColor, title: model.title, isActive: model.isActive)
cell.delegate = self
return cell
}
}

// SettingViewController

위 코드는 ViewController로 위 테이블 뷰 셀을 가지고 delegate를 자기 자신에게 연결해주고 있다.

이 때 Cell의 delegate가 Strong Referecing을 하고 있으므로 Retain Cycle이 생긴다. 이렇게 직접 코드를 확인해서 문제를 해결할 수 있지만, 사실 모든 코드를 전부 다 확인하기는 어렵다.

이러한 경우를 찾기 위해 XCode의 Instrument, Memory 분석 도구가 있다.

Debug Session

하나는 Debug Session이다. 앱을 디버그 모드로 실행하면 왼쪽 사이드바에서 메모리 사용을 간단히 확인할 수 있다.

앱을 실행하고 왼쪽 사이드바에 오른쪽 두 번째 아이콘 클릭
오른쪽 아이콘 클릭하면 View…드롭다운 메뉴가 보인다.

위 코드대로 Retain Cycle을 만들고 앱을 실행한 후 Debug Session을 열고 View Memory Graph Hierarchy를 선택해준다.

그리고 문제가 있는 SettingViewController 를 present시키고 dismiss 시키고를 여러 번 반복한다. 앱에서 해당 화면을 열고 닫고 하면 된다.

그리고 사이드바를 확인하면

오른쪽에 숫자가 1이 아닌 부분들을 확인해보자.

SettingViewController와 SettingSwtichTableViewCell이 5개나 메모리에 올라가 있는 것을 확인할 수 있다. 화면은 하나이기 때문에 굳이 여러개가 메모리에 올라가 있을 필요가 없는데 말이다.

protocol SettingSwitchTableViewCellDelegate: AnyObject {
func toggle(_ next: Bool)
}

class SettingSwtichTableViewCell: UITableViewCell {

static let identifier = "SettingSwtichTableViewCell"

weak var delegate: SettingSwitchTableViewCellDelegate?
...
}

// SettingSwitchTableViewCell

다시 weak 선언을 해주고, 위 메모리 분석을 다시 열고, SettingViewController를 여러번 열었다 닫았다를 반복해보았다.

이번엔 SettingViewController와 SettingSwitchTableViewCell이 한 개씩만 올라가 있는 것을 확인할 수 있었다.

Instruments 활용

이번엔 Instruments를 활용해서 좀 더 자세한 정보와 함께 확인해보자.

앱을 Cmd + I로 실행하면 분석 준비를 한 후 앱이 빌드된다.

위와 같이 나오는데, 다양한 분석툴이 준비되어 있다. 이번엔 Allocations를 선택해주었다. Instruments 화면이 나오면 좌측 상단 Record 버튼을 눌러주면 앱이 실행되며 모니터링도 실행된다.

위와 같은 화면이 나오는데, 그래프를 통해 시간 별로 메모리 변화를 시각적으로 확인할 수 있으며 아래 Swift 파일이름과 함께 어떤 파일이 얼마나 Persist되어 있는지, 어느정도의 메모리를 차지하고 있는지도 자세히 시간별로 확인할 수 있다. 또 좌측 하단 Filter 창을 통해 파일 이름으로 필터링해서 정보를 볼 수도 있다.

다시 테스트를 해보았다.

weak 선언을 하지 않고 똑같이 SettingViewController를 여러번 들어갔다 나갔다를 반복해주었다.

그래프를 확인하면 메모리가 조금씩 쌓여가는 것을 확인할 수 있었다.

다시 weak 선언을 하고 앱을 재실행 한 후 똑같은 행동을 반복해 보았다.

Filter에 SettingViewController로 검색을 한 상태

그래프를 보면 메모리를 사용하고 해제하고 사용하고 해제하는 것처럼 보인다. 또 Filter에 검색을 해도 마지막에는 SettingViewController가 Allocate되지 않았다는 것을 확인할 수 있었다.

--

--

peppermint100
PEPPERMINT100

기억하기 위해 또는 잊어버리기 위해 작성하는 블로그입니다.