Memory Leak 메모리 누수
🤔 메모리 누수란?
🙋♂️
프로그램이 더 이상 사용하지 않는 메모리를 해제하지 않고, 계속 점유하고 있는 상태를 말합니다.
이때, Swift에서 ARC의 알아야 됩니다.
ARC: Swift는 자동 참조 카운트를 사용해서 메모리를 자동으로 관리하는데,
각 객체는 참조 카운트를 가지고 있으며, 해당 객체를 참조하는 다른 객체가 있을 때마다 카운트가 증가됩니다.
그리고 참조 카운트는 0이 되면 객체가 메모리에서 자동으로 해제가 됩니다.
다음으로
강한 순환 참조는 두 개 이상의 객체가 서로를 강하게 참조하여, 그 결과 참조 카운트가 0이 되지 않아 객체가 메모리에서 해제되지 않는 상황을 말합니다. 이때 메모리 누수가 발생합니다.
메모리 누수를 가만히 두면, 프로그램의 성능을 저하시키고 심각한 경우에는 앱이 강제 종료될 수 있습니다.
그래서 iOS 앱 개발에서 메모리 누수는 중요한 문제입니다.
🤔 메모리 누수는 어떻게 해결하는데?
🙋♂️
가장 중요한 방법은 강한 순환 참조를 피하는 것입니다.
강한 순환 참조: 두 개 이상의 객체가 서로를 강하게 참조하면서 서로의 메모리를 해제하지 못하는 상황
weak 약한 참조와 unowned 미소유 참조 두 가지 방법으로 해결할 수 있습니다.
공통적으로 RC가 증가되지 않습니다.
1. weak 키워드
참조하고 있는 객체가 해제되면 자동으로 nil로 설정됩니다. 타입은 항상 옵셔널타입이어야 합니다.
A객체가 B객체를 참조하는데, B객체가 먼저 해제될 가능성이 있는 경우에 사용합니다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person? // 수명이 더 짧은 인스턴스를 가리키는 프로퍼티를 약한참조 선언한다.
deinit { print("Apartment \\(unit) is being deinitialized") }
}
var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
위 코드의 흐름을 살펴보면,
- 각각 클래스의 인스턴스가 만들어집니다.
- 존이 아파트를 강하게 참조하고, 유닛은 세입자를 약하게 참조하고 있습니다.
- 존을 nil로 설정하면, 존은 더 이상 인스턴스를 가리키지 않습니다.
- Person 객체에 대한 강한 참조가 없어지므로, 메모리에서 해제됩니다.
- 아파트는 세입자 프로퍼티가 약한 참조이므로,
- 존이라는 인스턴스가 메모리에서 해제되면 자동으로 세입자 프로퍼티는 nil로 됩니다.
- 유닛객체도 nil로 설정하면, 메모리에서 해제가 됩니다.
쉽게 예를 들면, UIViewController와 그 하위 뷰 컨트롤러 간의 관계에서 부모 뷰 컨트롤러는 자식을 강하게 참조하고, 자식은 부모를 약하게 참조하여 강한 순환 참조를 방지합니다.
2. unowned 미소유 참조
참조하는 객체가 항상 메모리에 있다고 확신할 수 있는 경우에만 사용합니다.
A객체가 B객체를 참조하고, B객체의 생명주기가 같거나 길다고 판단할 때 사용합니다.
참조하던 인스턴스가 만약 메모리에서 해제된 경우,
nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있기 때문에 런타임 에러가 발생합니다.
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
deinit { print("\\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // 비교적 수명이 더 긴 인스턴스를 가리키는 프로퍼티를 미소유참조 선언한다.
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\\(number) is being deinitialized") }
}
var john: Customer? = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// John Appleseed is being deinitialized
//Card #1234567890123456 is being deinitialized
- 존이 nil로 설정되면, Customer 인스턴스에 대한 강한 참조가 해제되어, 메모리에서 해제됩니다.
- CreditCard의 customer 미소유 참조는 해당 인스턴스를 가리키고 있습니다.
- 이때, Customer인스턴스가 해제되었기 때문에, CreditCard인스턴스도 해제됩니다.
🤔 클로저의 강한 순환 참조?
🙋♂️
먼저 클로저는 코드 블록을 모아놓은 것을 말합니다.
클래스 안에 클로저가 있을 때, 클로저 내부에서 self를 사용하여 클래스의 프로퍼티나 메서드를 접근하면
클로저 → self(클래스) 방향처럼 강하게 참조합니다.
동시에,
클래스 → 클로저 방향처럼 클로저를 프로퍼티로 가지고 있기 때문에, 강하게 참조합니다.
여기서 왜 강하게 참조하냐면, Swift에서 기본적으로 객체의 인스턴스나 프로퍼티를 생성, 사용할때
강하게 참조하는 것이 default로 설정되어 있기 때문입니다.
다시 돌아와서
클래스와 클로저가 서로를 강하게 참조하고 있습니다.
그래서 메모리 누수가 발생하기 때문에, 이를 해결해줘야 합니다.
다음은 클로저의 강한 순환 참조와 이를 해결한 두 가지 상황을 살펴보겠습니다.
weak를 사용하면 nil로 자동으로 설정됩니다.
→ 이는 옵셔널 타입으로 해야 된다는 것을 앞에 설명했습니다.
그러므로, 내부에서는 옵셔널 바인딩이나 옵셔널 체이닝으로 작업을 수행해야 합니다.
옵셔널 체이닝:
- nil이 아닌 경우에만 프로퍼티, 메서드, 서브스크립트에 접근할 수 있고,
- nil이면 그 이후의 모든 접근을 무시하고 nil이 됩니다.
정리
unowned도 옵셔널 타입이 가능해졌는데,
weak는 자동으로 nil로 설정돼서 안정성 있게 구현할 수 있는 반면에,
unowned 옵셔널타입은 자동으로 nil이 설정되지 않기 때문에 잘못된 메모리 접근 시 런타임 에러가 발생할 수 있습니다.
그래서 저는 weak 사용이 비교적 사용 편의성과 안정성이 높기 때문에 더 적합하다고 생각합니다.
이제 메모리에 대한 관심도 높아져서, 그중에 대표주제인 메모리 누수에 대해 공부하였습니다.
Swift는 ARC로 메모리를 자동으로 관리해 주기 때문에, 강한 순환 참조가 발생하고, 이로 인해 메모리누수가 발생하며,
해결하기 위해서 weak와 unowned 키워드인 두 가지 방법이 있으며, 왜 weak를 선호하고
왜 클로저에 weak self를 하고, 거기까지의 흐름을 이해하고, 전체적인 큰 틀을 정리할 수 있었습니다.
Reference
https://bbiguduk.gitbook.io/swift/language-guide-1/automatic-reference-counting#weak-references
https://babbab2.tistory.com/27