iOS

메모리 누수(Memory Leak) 점검 • 해결 방법 및 회고

하노 Hano 2023. 7. 29. 17:28

안녕하세요, 하노입니다 :D

 

오늘은 제가 겪었던 메모리 누수에 대한 점검 및 해결 방법과 짤막한 회고를 해보려고 합니다!

 

특정 화면에 대하여 리팩토링을 하던 중 약 1.5~6MB의 메모리가 계속 쌓이는 것을 확인하였습니다.

이에 대해 제가 어떤 방법으로 해결하려 했는지, 방법과 회고에 대해서 다뤄보겠습니다!

 


1. 메모리 누수(Memory Leak) 이란?

프로그램(앱)에서 사용한 메모리를 정상적인 방법으로 해제하지 못한 경우, 해당 메모리가 계속해서 남아있는 현상.

 

 

 

2. 메모리 누수의 주요 원인.

메모리 누수의 주요 원인은 메모리 영역과 큰 관련이 있습니다.

메모리 영역 중 Heap 영역에 해당되는 객체들에 한하여 메모리 누수 현상이 발생하는데요,

이 메모리 영역을 생각하면서 원인을 크게 2가지로 나누어 보겠습니다.

 

  • 강한 참조 사이클 (Strong Reference Cycle)
  • 클로저의 캡처 리스트 (Capture List)

 

강한 참조 사이클 (Strong Reference Cycle)

강한 참조 사이클은 두 개 이상의 객체가 서로 강한 참조를 하고 있는 경우 발생합니다.

메모리 누수는 Heap 영역에서 발생한다고 했으니,

보통 두 개 이상의 Class(혹은 Heap 영역을 사용하는 객체)가 서로 강한 참조를 하고 있는 경우 발생합니다.

 

 

 

클로저의 캡처 리스트 (Capture List)

클로저의 특성 중 하나인 캡처 리스트가 종종 (때때로 많이..) 메모리 누수를 발생시키곤 합니다.

클로저 내에서 Heap에 저장되는 메모리를 사용할 경우 발생합니다.

 

 


 

3. 메모리 누수 점검 방법

이제 본격적으로 메모리 누수 위치를 찾을 차례입니다.

누수 점검 방법으로는 총 5가지 정도의 방법이 있는 것으로 알고 있습니다.

 

 

  1. Debug Navigator에 찍히는 메모리를 직접 보면서 찾는다.
  2. deinit 메서드
  3. Debug Memory Graph
  4. Instruments의 Leaks
  5. CFGetRetainCount() 함수

 

과연 정말로 한가지씩 직접 해보면서 실제로 메모리 누수를 찾을 수 있는지 알아보겠습니다!

 

 

 

 

 

1. Debug Navigator로 Memory 확인하기.

이름은 그럴듯 하지만 제일 직관적이면서 쉽고, 누수 발생을 확인하기 위한 가장 첫 단계라고 생각합니다.

말 그대로 Debug Navigator를 통하여 눈으로 메모리가 늘어나는지 안 늘어나는지 확인하는 방법입니다.

 

XCode를 켜서 앱을 실행해보시면 좌측에 스프레이를 뿌리는 아이콘을 클릭하시면 위 사진과 같이 나옵니다.

넵... 저게 Debug Navigator입니다... 저도 글을 쓰면서 처음 용어를 알게 되었네요...ㅎㅎㅎ

 

이제 화면을 push pop 해보면서 메모리가 늘었다 줄어들었을 때, 이전 메모리와 크기가 같은지 확인해 보시면 됩니다.

끝입니다. 참 쉽죠??? 이 방법은 직관적이지만 메모리 누수가 발생하는 곳을 정확하게 찾을 순 없습니다.

다만, 어느 화면에서 메모리 누수가 발생한다. 정도는 유추해볼 수 있습니다.

 

 

 

2. deinit 메서드를 사용해서 확인하기.

메모리 영역 중 Heap 영역을 사용하는 객체에서 메모리 누수가 발생한다고 했으니,

반대로 말하면 메모리 누수가 발생하지 않는 객체는 메모리가 해제되기 전 deinit 함수가 제대로 동작해야겠네요!

 

앞서 말한 특성으로 메모리 누수가 의심되는 객체에 deinit을 찍어보면서 실제로 메모리 해제가 되는지 찾는 방법입니다.

 

deinit {
	print("\(self) is deinit !!")
}

보통 저는 객체를 출력하는 간단한 코드로 메모리 해제가 되는지 찾습니다.

 

만약 코드의 양이 적고 간단한 경우 deinit을 사용하여 쉽게 찾을 수 있지만,

코드가 양이 많고 복잡할 경우 정확하게 어느 부분에서 메모리 누수가 발생하는지 알 수 없습니다.

 

따라서 보통 이미 누수가 발생하는 위치를 해결하고 마지막에 제대로 해결되었는지 검증하는 방법으로 주로 쓰고 있습니다.

 

 

 

 

 

3. Debug Memory Graph를 활용하여 찾기.

 

만약 코드가 정말 복잡하거나 강한 참조가 발생하는 곳을 빠르게 찾고 싶을 때 적극적으로 사용하는 방법입니다!

 

우선 Memory Leak이 발생시키는 코드를 만들어보겠습니다.

 

class AClass {
    var property: BClass?
}

class BClass {
    var property: AClass?
}

class PushedViewController: UIViewController {
    
    var a = AClass()
    var b = BClass()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        a.property = b
        b.property = a
    }
}

 

 

저는 NavigationController로 Push - Pop 동작 시 Memory Leak을 발생시키게 했습니다.

 

Push - Pop 동작 후 아래 사진의 빨간색 박스에 있는 공유 버튼(?)처럼 생긴 것을 누르면 그래프가 뜹니다.

 

 

 

그럼 실제로 Memory Leak이 발생하는지 그래프로 확인해보겠습니다.

 

 

뭔가 딱 봐도 AClass와 BClass가 서로 엮여있는 것처럼 보이네요!

이때 화살표가 굵은 선이면 강한 참조, 얇은 선이면 약한 참조를 의미합니다.

 

이제 저 굵은 색 화살표를 클릭해 보면 우측 Inspecter 영역에 Reference Details가 나오는데,

Type을 보면 Strong이라고 나옵니다.

 

 

 

또한, 아래 그림처럼 좌측 Debug Navigator의 하단에 Filter 쪽 warning 마크로 되어있는 show only leaked locations를 클릭해보면 어느 부분에서 강한 참조가 발생하고 있는지 필터로 확인이 가능합니다!

이러면 굳이 객체들을 하나씩 클릭해 볼 필요 없이 바로 찾을 수 있겠죠???

 

 

 

이렇게 어느 부분에서 강한 참조가 발생하는지 화살표를 통해 직관적으로 확인해 볼 수 있습니다!

 

 

 

 

 

4. Instruments의 Leaks를 활용하여 찾기

앞에서 다룬 Debug Memory Graph와 거의 동일하게 어느 부분에서 강한 참조가 일어나는지 정확하게 파악해 주는 방법입니다!

실제로 사용해 보면

어?? 이런 부분까지 찾아서 보여준다고???

라고 생각할 정도로 자세하게 파악해 줍니다!

한번 바로 사용해 보죠!

 

코드는 Debug Memory Graph와 같은 코드로 사용하겠습니다!

다만 여기서 빌드런을 하는 것이 아니라 comand + i 버튼을 눌러 Instruments로 실행하게 해 줍니다!

그럼 아래 그림처럼 Instruments가 실행될 텐데요!

여기서 Leaks를 선택하겠습니다.

 

그럼 아래 그림처럼 창이 하나 뜰 텐데,

여기서 좌측 상단의 빨간색 녹화버튼 (Start an immediate mode recoding)을 클릭해 주면 코드가 실행됩니다.

그리고 Push - Pop 동작을 하면 아래와 같이 Memory Leak이 발생했다는 표시가 출력됩니다!

 

 

그럼 저 불길한 X 마크를 클릭해보면 Memory Leak이 어디서 발생했는지 알려줍니다.

 

 

저는 AClass와 BClass에서 2번 Memory Leak이 발생했다고 알려주네요.

하지만

나는 Debug Memory Graph처럼 화살표로 친절하게 표시해 주는 그래프 형태로 보고 싶어!

하신다면,

아래 그림처럼 Cycles & Roots를 눌러주시면 Instruments에서 친절하게 Graph 형태로 표시해 줍니다.

 

 

실제로 그래프를 보기 위해 클릭해 보면,

 

 

 

위 그림처럼 그래프로 알려줍니다!

그래프를 해석해 보면,

AClass의 property가 BClass를 참조하고 있고, BClass의 property가 AClass를 참조하고 있다고 해석할 수 있겠습니다!

 

 

이처럼 Instruments의 Leaks를 통해 Memory Leak이 어떤 부분에서 발생하는지 자세하게 확인해 볼 수 있습니다.

또한, 코드가 복잡하게 얽혀있는 경우, 코드의 어느 부분에서 Memory Leak이 발생하는지까지 직접 확인해볼 수 있습니다.

 

 

 

5. CFGetRetainCount로 참조 횟수 확인하여 찾기

사실 이 방법은 코드를 직접 작성하고 수정해 가면서 찾아야 하기 때문에 자주 사용하는 방법은 아니라고 생각합니다.

(실제로 저는 이번에 Memory Leak을 확인할 수 있는 방법을 찾아보면서 처음 알았습니다...!)

 

사용하는 방법은 아래 함수를 사용하여 Print 해보는 방법입니다!

CFGetRetainCount(_ cf: CFTypeRef!) -> CFIndex

 

 

마찬가지로 위에서 사용했던 동일한 코드로 사용 후 콘솔에 출력해 보면,

 

AClass와 BClass의 Reference Count가 3으로 나오네요!

정상적으로 메모리가 해제될 때 Reference Count는 2가 되어야 하는데 3이면 확실히 Memeory Leak이 발생한다는 뜻입니다!

 

 

이처럼 CFGetRetainCount 함수를 통해서 Reference Count를 직접 출력해 보면서 Memory Leak이 발생하는 property를 파악해볼 수 있습니다.

하지만 앞서 말했듯이 출력하는 코드를 직접 작성해야 하고 Class에서 사용하는 Property를 직접 하나씩 출력해보면서 확인해야 합니다.

 

 

 

 


 

4. 회고

 

위에서 살펴본 5가지의 방법 중 4가지 방법을 통해 Memory Leak이 어느 곳에서 발생하는지 파악할 수 있었고,

결과적으론 클로저에서 강한 참조가 발생하는 것을 확인하여 이 부분의 코드를 수정하여 Memory Leak을 해결할 수 있었습니다!

 

저는 4가지 방법 중 Debug Memory Graph와 Leaks를 주로 사용하여 다행히 Memory Leak을 찾았었는데요,

이게 꼭 100%를 보장하지는 않는다고 합니다... 그래서 꼭 Memory Leak을 생각하면서 코드를 작성하자... 라는 교훈을 얻었습니다.

 

 

 

 


 

 

 

 

오늘은 Memory Leak에 대해 알아봤습니다!

사실 이번 Memory Leak 글을 작성하면서, 새로 알게 된 사실도 있고, 저도 다시 한번 정리해 볼 수 있는 시간이었습니다!

 

혹시라도 글을 읽으면서 의견이 다르거나 틀리다고 생각하시는 분들은 적극적으로 댓글 달아주시면 감사하겠습니다!!

 

그럼 오늘 글은 여기서 마치겠습니다. 감사합니다 :D