메모리 누수(Memory Leak) 현상은 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상입니다.
이처럼 메모리 누수 현상이 지속되면 메모리(Heap) 공간이 꽉 차게 되어 프로그램이 다운되는 현상이 발생할 수도 있습니다.
Swift에서 메모리(Heap) 공간의 관리는 ARC(Automatic Reference Counting) 모델에 의해 자동으로 관리되지만, 강한 참조 사이클(Strong Reference Cycle)로 정의된 데이터는 카운트 값이 계속 유지될 수 있기 때문에 주의해야 합니다.
Swift에서 ARC는 컴파일러에 의해 자동으로 작동됩니다.
하지만 강한 참조 사이클에 의해 데이터가 메모리(힙)에 제거되지 않고 계속 남는 현상이 발생해서 메모리 누수가 발생하는 현상이 일어날 수 있습니다.
⚙️ 강한 참조
인스턴스(객체) 또는 클로저가 한쪽으로 인스턴스(객체) 또는 클로저를 지목하여 참조 (단방향 참조)
⚙️ 강한 참조 사이클
인스턴스(객체) 또는 클로저가 서로(쌍방향)를 지목하여 참조
✅ 강한 참조 사이클에 의한 메모리 누수 사례
class Boy{ var girlfriendName: Girl? deinit{ // 소멸자를 사용하여 참조 카운트 값이 0이 되었을 때 작동 print("인스턴스(객체)의 참조 카운트 값이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다.") } } class Girl{ var boyfriendName: Boy? deinit{ // 소멸자를 사용하여 참조 카운트 값이 0이 되었을 때 작동 print("인스턴스(객체)의 참조 카운트 값이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다.") } } var kim: Boy? = Boy() // kim 참조 카운트 1 증가 var lee: Girl? = Girl() // lee 참조 카운트 1 증가 // 각각의 인스턴스(객체)가 서로를 참조 kim?.girlfriendName = lee // kim 참조 카운트 1 증가 -> kim의 총 카운트 = 2 lee?.boyfriendName = kim // lee 참조 카운트 1 증가 -> lee의 총 카운트 = 2 // 서로 참조를 했기 때문에 인스턴스(객체)를 nil로 초기화해도 완벽하게 참조 카운트 값을 0으로 만들 수 없음 kim = nil // kim 참조 카운트 1 감소 -> kim의 총 카운트 = 1 lee = nil // lee 참조 카운트 1 감소 -> lee의 총 카운트 = 1
강한 참조 사이클에 의한 메모리 누수를 해결/예방하는 방법은 크게 3가지로 나뉩니다.
1️⃣ 강한 참조 사이클이 일어날 코드를 작성하지 않는다. (추천 X)
2️⃣ 약한 참조(Weak Reference)를 활용하여 코드를 작성한다.
3️⃣ 비소유/무소유 참조(Unowned Reference)를 활용하여 코드를 작성한다.
✅ 약한 참조(Weak Reference)
- 약한 참조는 서로를 가리키는 인스턴스(객체)의 카운트 결과를 세지 않는 방식입니다. (비소유/무소유 참조와 같은 개념)
- 약한 참조는 소유자(상위 인스턴스)보다 짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용합니다.
- 참조하고 있던 인스턴스가 메모리에서 제거되면, 참조했던 다른 한쪽의 인스턴스는 nil로 초기화됩니다.
- 약한 참조는 변수(var)로만 정의할 수 있고, 옵셔널 타입으로만 정의해야 합니다.
- 약한 참조로 사용할 변수를 선언하기 위해서는 변수 앞에 weak 키워드를 작성해야 합니다.
weak var 변수명: 옵셔널 타입
class Boy{ weak var girlfriendName: Girl? // 약한 참조 타입으로 변수 생성 deinit{ // 소멸자를 사용하여 참조 카운팅이 0이 되었을 때 작동 print("인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다.") } } class Girl{ weak var boyfriendName: Boy? // 약한 참조 타입으로 변수 생성 deinit{ // 소멸자를 사용하여 참조 카운팅이 0이 되었을 때 작동 print("인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다.") } } var kim: Boy? = Boy() // kim 참조 카운팅 1 증가 var lee: Girl? = Girl() // lee 참조 카운팅 1 증가 // 각각의 인스턴스(객체)가 서로를 참조 kim?.girlfriendName = lee // 약한 참조에 의해 카운팅 X -> kim의 총 카운팅 = 1 lee?.boyfriendName = kim // 약한 참조에 의해 카운팅 X -> lee의 총 카운팅 = 1 // 약한 참조에 의해 참조 카운트 값을 0으로 만들 수 있음 kim = nil // kim 참조 카운팅 1 감소 -> kim의 총 카운팅 = 0 print(lee?.boyfriendName) // 약한 참조에 의해 lee?.boyfriendName의 값이 nil로 초기화 lee = nil // lee 참조 카운팅 1 감소 -> lee의 총 카운팅 = 0 /* 출력 결과 인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다. nil 인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다. */
✅ 비소유/무소유 참조(Unowned Reference)
- 비소유/무소유 참조는 서로를 가리키는 인스턴스(객체)의 카운트 결과를 세지 않는 방식입니다. (약한 참조와 같은 개념)
- 비소유/무소유 참조는 소유자(상위 인스턴스)보다 길거나 같은 생명주기를 가진 인스턴스를 참조할 때 주로 사용합니다.
- 참조하고 있던 인스턴스가 메모리에서 제거되면, 참조했던 다른 한쪽의 인스턴스는 nil로 초기화되지 않습니다.
- 비소유/무소유 참조는 변수(var), 상수(let) 둘 다 정의할 수 있고, 다양한 타입으로 정의할 수 있습니다.
- 비소유/무소유 참조로 사용할 변수(상수)를 선언하기 위해서는 변수(상수) 앞에 unowned 키워드를 작성해야 합니다.
unowned var 변수명: 다양한 타입
class Boy{ unowned var girlfriendName: Girl? deinit{ // 소멸자를 사용하여 참조 카운팅이 0이 되었을 때 작동 print("인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다.") } } class Girl{ unowned var boyfriendName: Boy? deinit{ // 소멸자를 사용하여 참조 카운팅이 0이 되었을 때 작동 print("인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다.") } } var kim: Boy? = Boy() // kim 참조 카운팅 1 증가 var lee: Girl? = Girl() // lee 참조 카운팅 1 증가 // 각각의 인스턴스(객체)가 서로를 참조 kim?.girlfriendName = lee // 비소유/무소유 참조에 의해 카운팅 X -> kim의 총 카운팅 = 1 lee?.boyfriendName = kim // 비소유/무소유 참조에 의해 카운팅 X -> lee의 총 카운팅 = 1 // 비소유/무소유 참조에 의해 참조 카운트 값을 0으로 만들 수 있음 kim = nil // kim 참조 카운팅 1 감소 -> kim의 총 카운팅 = 0 lee = nil // lee 참조 카운팅 1 감소 -> lee의 총 카운팅 = 0 /* 출력 결과 인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다. 인스턴스(객체)의 참조 카운팅이 0이 되어 해당 데이터가 메모리(Heap)에서 제거되었습니다. */