swift 의 class 는 Referance 타입이고, ARC 를 통해서 메모리를 관리하기 때문에 ARC 를 알아야 한다고 한다.
💾 ARC의 객체 수명
object life time and ARC
- RC 는 init 되는 시점에 1이 된다.
- 객체의 생명주기는 init 을 할 때 시작되고
- 마지막 사용이 끝난 뒤에 해제 된다.
- ARC 가 retain 과 release 를 자동으로 넣어준다.
ARC 를 사용하기 전에는 retian 과 release를 사용해서 사용자가 RC 를 올려주고 내려주는 작업을 했다.
ARC 를 사용한 이후로 컴파일러가 코드를 읽고 적당한 부분에 retain 과 release 를 삽입해주고 있다.
자 wwdc 에서 보여준 예제를 보자.
class Traveler {
var name: String?
var destination: String?
}
func test() {
let traveler1 = Traveler(name: "Lilly")
// retain
let traveler2 = traveler1
// release
traveler2.destination = "Big Sur"
// release
print("Don travelling")
}
- test 메서드 안에서 traveler1 을 만들었다. 이 시점에 초기화가 되고 RC 는 1이 된다.
- traveler2에 traveler1 을 할당했다. 할당이 일어났으니 RC 를 1 늘리는 작업이 필요하다. 컴파일러가 런타임에 retian 을 넣어주어 RC가 1이 잘 증가했다.
- 그 라인 이후로 메서드가 끝날 때까지 traveler1이 사용되는 일은 없다. 컴파일러는 그래서 release 를 호출해주어서 RC를 하나 줄여 준다.
- traveler2.destination 의 값을 변경했다. 이 이후에 traveler2 도 사용할 일이 없다. 그래서 컴파일러가 자동으로 release 를 해주었다.
- RC 가 0이 되었으므로 메모리에서 모든 값이 잘 해제 되었다.
스위프트의 객체 수명은 객체가 초기화 될때 그리고 마지막으로 사용될 때로 보장된다.
이게 C++ 과 다른 점이다.
C++ 을 배운적이 없어서 뭐가 다르다는 건지 모르겠다.
아시는분 좀 알려주세요!
메서드가 끝나는 } 중괄호 에서는 확실히 값이 메모리에서 해제 되었다는 것을 보장한다.
이런 이야기를 하는 이유는 항상 마지막 사용 바로 뒤에 메모리에서 값이 해제 되지는 않는다는 것이다.
컴파일러가 최적화를 하면서 그 작업을 바로 할 수 도 있고 미뤄질 수도 있다.
하지만 결국에 } 전에는 확실히 값을 해지하겠다고 약속했다는 것이다.
사실 strong RC 를 사용할 경우엔 언제 값이 해지되는지가 그렇게 중요하진 않다.
그런데 weak 이나 unowned 를 쓰면 약간 이야기가 달라진다.
ARC를 너무 믿으면 안되는 이유이다. 우리는 컴파일러가 어떻게 처리하고 있는지를 모른다. 그 로직은 언제든 바뀔 수도 있다. 그래서 만약 우리가 ARC 가 해주는 (객체 수명을 관측해서 release 해주는) 일을 믿고 코드를 작성했다. 아무 이상 없었다. 그건 그냥 우연인 것이다. 나중에 컴파일러나 소스가 없데이트 되면 이러한 부분에서 오류가 생길 수도 있다. 이 점을 인지해야 한다는 것이다.
이에 대한 자세한 예시를 다음 챕터에서 부터 나온다.
Observable(관측가능한) object lifetime
language features
Consequences and safe techniques
👀 관측가능한 객체 수명
👩❤️👨 순환참조
이 코드의 예제를 보면 우리가 익히 아는 순환 참조가 일어나고 있다.
test 메서드를 한줄 한줄 씩 보자.
1️⃣ travler
인스턴스가 heap에 생성됨. travler
의 RC 1
2️⃣ account
인스턴스가 heap에 생성됨. account
의 RC 1 account
가 travler
를 참조함. travler
의 RC 2
3️⃣ travler
가 account
참조함, account
의 RC 2. account
의 마지막 사용이므로 release 됨. account
의 RC 1.
4️⃣ travler
의 마지막 사용이므로 release 됨. travler
의 RC 1.
test 메서드가 끝나고 traveler 와 account 를 모두 release 해주었지만, 서로를 참조하고 있던 RC 가 남아있어서 메모리는 해지되기 못해서 누수가 일어나는 대참사가 일어났다.
나는 개인적으로 이 순환 참조 부분이 이해가 갔다가도 다시 보면 굉장히 헷갈렸었다.
왜냐면 객체를 해제할 때 프로퍼티에 있는 값들도 다 사라지는 것인데 왜 RC 를 안 지우지? 싶어서 헷갈렸는데 retain release 를 통해 컴파일러가 처리해준다는 사실을 아니 안햇갈린다.
🥊 순환참조 부수기
strong RC 를 사용할 경우엔 언제 값이 해지되는지가 그렇게 중요하진 않지만 weak 이나 unowned 를 쓰면 중요하다고 했다.
왜 그럴까?
weak and unowned 에 대한 설명은 아래와 같다.
weak and unowned referances
- RC에 관여하지 않는다.
- reference cycles 을 부신다.
- 참조된 객체가 사용중에 메모리에서 해제 될 수 있다.
- weak reference 접근하면 nil 을 밷는다.
- unowned reference 접근하면 오류가 난다.
사용중에 메모리에서 해제 될 수 있다고??? 코드를 보자.
위의 순환 참조가 일어났던 코드에서 Account 의 traveler 를 weak 으로 변경해주면 이런 문제는 해결된다.
우리가 weak RC 를 사용해서 reference cycles 을 부순 것이다.
class Traveler {
var name: String
var account: Account?
}
class Account {
weak var traveler: Traveler?
var points: Int
func printSummary() {
print("\(traveler.name) has \(points) points")
}
}
func test() {
let traveler = Traveler(name: "Lilly")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
//traveler 의 사용이 끝나서 release 함
account.printSummary()
}
test 메서드의 traveler.account = account 부분에서 traveler 의 사용은 끝났다.
컴파일러가 release 를 해주었고, account 는 Traveller 를 weak 으로 가지고 있기 때문에 RC 로 치지 않아서 traveler 는 메모리에서 해지 되었다.
그 이후에 우리는 account.printSummary() 를 해주었다. 이 메서드에는 traveler.name 을 들고 와야 하는데 traveler 가 메모리에서 해지 되어서 들고 올 수가 없다. weak 이 였다면 optional 을 unowned 였다면 크래시가 났을 것이다.
이런 경우가 생길 수 있어서 참조된 객체가 사용중에 메모리에서 해제 될 수 있다
고 한 것이다.
🥊 라이프 타임 오류 부수기
optional binding 하기
optional binding 을 하면 런타임 오류가 나지 않을 수 있다! 라고 혹시 생각했는가?
런타임 오류는 나지 않을 수 있다. 그런데 그게 궁극적인 해결 책은 아니다. 오히려 optional binding 을 썻다가 버그를 인지하지 못해서 문제를 더 키울 수도 있다.
withExtendedLifetime() 메서드 사용
withExtendedLifetime() 메서드의 바디가 끝날 때 까지 받은 파라미터의 lifetiem 을 늘주는 메서드다.
아래와 같은 방법으로 사용할 수 있다.
func test() {
let traveler = Traveler(name: "Lilly")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
withExtendedLifetime(traveler) {
account.printSummary()
}
}
func test() {
let traveler = Traveler(name: "Lilly")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
account.printSummary()
withExtendedLifetime(traveler) {}
}
func test() {
let traveler = Traveler(name: "Lilly")
let account = Account(traveler: traveler, points: 1000)
defer {withExtendedLifetime(traveler) {}}
traveler.account = account
account.printSummary()
}
그런데 이 기술은 위험하고 한다.
weak 을 사용할 때마다 이 메서드를 호출해 줘야 하고 그럼 유지 보수도 까다롭고 비용이 많이 든다.
weak/unowned reference 피하도록 구조 다시 짜기
애초에 weak과 unowned 를 사용해서 생기는 문제였다면 저 둘을 사용하지 않도록 구조를 다시 짜면 된다.
이와 같이 personalInfo 객체를 하나 더 만들어 주었더니 순환 참조가 없어져서 weak 이나 unowned 쓰지 않아도 되도록 구조가 바뀌었다.
구현을 더 해줘야 하는 것이 단점이지만 장기적으로 봤을 땐 이게 더 좋을 수도 있다.
🤦🏻 Deinitializer side-effects
라이프 타임 오류 부수기와 비슷한 맥락의 오류이다.
deinit 의 시점을 정확하게 모르니 생기는 오류 말이다.
코드를 보면 Lily is deinitializing
이 먼저 프린트 될지 Done traveling
이 먼저 프린트 될지 모르는 일이다. 컴파일러 마음이다.
만약 순서가 중요했다면, 이게 side-effects 라고 볼 수 있겠다.
다른 비슷한 예시를 보자.)
여기서도 비슷한 오류가 나타날 수 있다.
traveler
의 사용이 끝나고 바로 할당을 해제 했다고 하자.
deinit 에서 publish
를 실행해서 id 랑 category 를 보여 줘야 하는데 metrics.computeTravelInterest()
를 실행하기 전이라 설정 된 카테고리가 없다!
nil 을 출력하게 된다.
🥊 side-effects 부수기
위에서 제시한 방법과 같은 방법들을 사용해서 이를 해결 할 수 있다.
withExtendedLifetime() 메서드 사용
구조 다시 짜기
Private 이용travelerMetrics
를 private 으로 바꾸고 deinit 을 할 시 travelerMetrics.computeTravelInterest()
을 부르도록 구조를 변경해 주었다.
life cycle 자체를 고쳐준 것은 아니지만 오류가 나지는 않을 코드다.
life cycle 자체 수정
그럼 life cycle 자체를 수정 해보자.
위와 같이 assert 를 이용해 검사를 해주고, defer 키워드를 이용하면 life cycle 자체를 조정하면서 오류를 잡을 수 있다.
⚙️ xcode 설정
이걸 켜 놓으면 거의 컴파일러가 객체의 사용이 종료 되는 시점에 바로 해제를 시킨다고 한다. 컴파일러 일관성 측면에서 켜 놓으면 좋다구 그런 것 같다.
순환 참조에 대한 새로운 견해 였던 것 같다. weak 을 남발하지 말고 꼭 써야 할 곳에만 쓰자! 구조 자체를 수정해서 유지 보수를 쉽게 하자!
'🍎 iOS > 🧑🏻💻 WWDC' 카테고리의 다른 글
[WWDC19] SwiftUI Essentials (0) | 2023.01.10 |
---|---|
[WWDC] Architecting Your App for Multiple Windows (0) | 2023.01.06 |