ARC in Swift: Basics and beyond - WWDC21 - Videos - Apple Developer
ARC가 어떻게 동작하는지 살펴보는 시간
Object lifetimes and ARC
- Object lifetime은 init() 에서 시작, 마지막 사용에서 끝
- ARC는 object의 lifetime이 끝날때 deallocate 해줌
- ARC는 reference count로 object의 lifetime을 추적
- ARC는 Swift 컴파일러 주도로 이루어짐. 컴파일러가 retain/release operation을 넣음
- 런타임에 retain은 reference count를 증가시키고, release는 감소시킴. reference count 가 0이 되었을때 object는 deallacte 됨
- 이 과정이 어떤 식으로 이루어지는지 예제로 확인해봅시다!
여행앱을 만든다고 가정
class Traveler {
var name: String
var destination: String?
}
func test() {
let traveler1 = Traveler(name: "Lily")
let traveler2 = traveler1
traveler2.destination = "Big Sur"
print("Done traveling")
}
컴파일 타임에 일어나는 일들
먼저 traveler1
object를 타겟으로 살펴봄
traveler1
reference가 마지막에 사용되고 나서 컴파일러가 release
를 붙여줌
reference가 시작될 때 retain
operation을 붙여주지는 않는데, 왜냐하면 initialization이 reference count를 1로 만들어주기 때문. (아마도... 암시적 retain이라고 볼 수 있을 것 같아요)
다음은 traveler2
object를 타겟으로 살펴봄
이번에는 init() 으로 객체가 시작되지 않았기 때문에 컴파일러가 retain
을 붙여줌
마지막 사용 후에 release
를 붙여줌
런타임에서는 무슨일이?
런타임에서 일어나는 일도 살펴봅시다
처음에 Traveler object가 heap에 생성됨. reference count가 1인 상태로 init 됨.
새로운 reference 를 준비하기 위해, reatin operation이 실행됨.
reference count는 2가 됨.
traveler2
도 동일한 주소값을 가리키게 됨
traveler1
의 사용이 끝나고 나서 release
operation이 실행되게 됨
reference count는 1이 됨
object의 destination은 "Big Sur"로 업데이트 됨
traveler2
reference의 사용도 끝났기 때문에
release
operation이 실행됨
reference count는 0이 됨
reference count가 0이 되고나면
object는 deallocate 될 수 있음
Observable object lifetimes
Swift의 Object lifetime은 사용되는 것을 기반(use-based)으로 판단됨.
initialization에시 시작되고, 마지막 사용에서 끝이남
(당연한 소리 아닌가? 싶었는데...)
Swift에서는 닫는 중괄호에서 object의 lifetime이 끝나는 것을 보장받음
이게 C++ 언어와 다른점!
이 예제에서는 object가 마지막 사용 이후에 즉시 deallocate 되는 것을 확인할 수 있음
실제로는 ARC 최적화에 따라 즉시 deallocate 되지 않는 경우가 있음
원래는 이렇게 시작되고 끝나야 하는데
ARC 최적화에 따라서 lifetime이 조금 변경될 수 있음
object를 마지막에 사용한 이후로도 object가 살아있을 수 있음
대부분의 경우에는 정확한 lifetime이 중요하지 않음
하지만 이슈가 되는 경우도 있음
weak
,unowned
reference- deinit
observed object lifetime에 의존하는 경우, 나중에 문제가 될 수 있음
오늘은 기능이 작동하더라도 우연히 작동한 것일 수 있기 때문
Swift compiler의 detail이 변경되면 기능 lifetime이 바뀌게 될 수도 있음
그런 버그들은 개발 중에 발견되지 않을 수 있음
아무튼 변경될 수 있고, 버그 발생 가능성이 있다는 뜻
Weak and unowned references
- reference count에 관여하지 않음
- 주로 reference cycle을 발생시키지 않기 위해 사용됨
Reference cycle
class Travler {
var name: String
var account: Account?
func printSummary() {
if let account = account {
print("\(name) has \(account.points) points")
}
}
}
class Account {
var traveler: Traveler
var points: Int
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, point: 1000)
traveler.account = account
traveler.printSummary()
}
travler
object가 heap에 생성됨. reference count 1
account
object가 heap에 생성됨. reference count 1
account object가 traveler object를 참조 하고 있기 때문에,
traveler
object의 reference count 2 로 증가시킴
그리고traveler
가 account
를 참조하면서
account
의 reference count는 2가 됨
여기까지가 account
reference의 마지막 사용이기 때문에
account
의 reference count는 1로 감소함
traveler
object가 마지막으로 사용되고나서
traveler
의 reference count는 1로 감소함
object가 다 사용되고 나서도 object들의 reference count는 1로 남아있게 됨...
이게 바로 reference cycle이 발생하는 이유!
결과적으로 object가 절대 deallocate 될 수 없음
memory leak을 초래함
순환참조 해결
순환참조를 해결하기 위해서
weak, unowned reference를 사용할 수 있음
object가 deallocate 되었는데 object에 접근하게 되면
- weak reference는 nil을 반환
- unowned reference는 trap 처리
위의 예제에서 weak var traveler로 지정하면
traveler의 reference count가 0이 됨
traveler
의 reference count가 0이기 때문에
deallacte 될 수 있음
traveler
가 deallocate 되었기 때문에
account
의 reference count는 0으로 감소함
그러면 account
도 deallocate 될 수 있음
weak 사용하는 다른 예제
traveler.account = account 라인 이후 시점으로 보면
traveler
가 사용되는 마지막 지점이기 때문에 reference count가 0으로 감소함
traveler
는 deallocate 될 수 있음
이때 printSummary() 가 호출되면
traveler가 nil이기 때문에 크래시 발생할 수 있음!
optional binding으로 크래시는 해소할 수 있음
하지만 optional binding은 사실 문제를 더 심각하게 만들 수 있음
명확한 크래시가 안나기 때문에, 숨은 버그를 만들 수 있음
나중에 traveler
객체의 lifetime이 변경되었을 때 인지하기 어려워짐
이 문제를 해결할 수 있는 몇 가지 테크닉이 있음
withExtendedLifetime()
- 강한 참조를 통해 접근하도록 재설계
- weak/unowned를 피하도록 재설계
withExtendedLifetime()
(이런것도 있었구나...)
body closure가 완료될 때 까지traveler
object의 timetime을 늘려줌
잠재적 버그를 방지할 수 있음
아래와 같이 몇 가지 방법으로 쓸 수 있음
아래 예제에서는 동일한 효과
좋은 해결책으로 보이지만, 이 테크닉은 fragile함
weak reference가 잠재적 버그를 품고 있을 때 마다 withExtendedLifetiem()
을 호출해줘야 함
특별한 통제가 없다면 withExtendedLifetiem()
가 코드베이스 전반에 퍼질 수 있음 → 유지보수 비용 증가
Redesign to avoid weak/unowned reference
Redesign 하는 것이 나은 선택이 될 수 있음
잠시 멈춰서 생각할 필요가 있음
왜 weak / unowned reference가 필요할까?
순환 참조를 피하기 위해서만 사용되는가?
알고리즘을 다시 생각하거나 사이클 구조를 트리 구조로 변경해서 순환 참조를 피할 수 있음
기존의 디자인은 이렇게 되어 있음
사실 Account 클래스는 Traveler 클래스의 personal info 만 필요함
아래와 같은 구조로 변경할 수 있음
PersonalInfo만 새로운 클래스로 생성
이렇게 하면 Traveler 클래스와 Account 클래스가 PersonalInfo를 참조해도 순환참조가 발생하지 않음
weak/unowned가 필요없게 만들려면 추가적인 구현 비용이 들 수 있음
하지만 이렇게 디자인 하는 것이 잠재적 object lifetime bug를 명확히 줄이는 방향
Deinitializer side-effects
deinit에 side-effect가 있는 경우
ARC optimization에 따라 print가 "Done traveling" 이후에 호출될 수 있음
(deinit 으로 print 되는 순서를 보장할 수 없다는 의미인듯)
또 다른 예제를 봅시다
func test() {
let traveler = Traveler(name: "Lily", id: 1)
let metrics = traveler.travelMetrics // travelMetrics reference를 copy
// destination이 "Big Sur"로 업데이트 됨
// travelMetrics에 "Big Sur"가 기록됨
traveler.updateDestination("Big Sur")
// destination이 "Catalina"로 업데이트 됨
// travelMetrics에 "Catalina"가 기록됨
traveler.updateDestination("Catalina")
// 기록된 destination을 기반으로 interest category가 계산됨
metrics.computeTravelInterest()
}
verifyGlobalTravelMetrics()
오늘은 deinit이 travel interest를 compute 한 뒤에 실행되었다고 해보자
그러면 interested category를 Nature로 publish 함
하지만 traveler 객체가 마지막으로 사용된 것은
traveler.updateDestination("Catalina")
라인
deinit이 그 뒤에 즉시 호출될 수도 있음
computeTravelInterest() 전에 deinit이 호출된다면
nil이 publish되어서 버그가 발생할 수 있음
앞서 weak, unowned reference 예제에서 다뤘던것 처럼
아래와 같은 테크닉을 사용해서 이슈를 해결할 수 있음
withExtendedLifetime()
lifetime을 늘려줘야하는 부분에 withExtendedLifetime()
사용하기
앞서 얘기 했던것 처럼 deinit side-effect가 발생할 수 있는 모든곳에 이런 처리를 해줘야 함...
유지보수 비용 증가함
Redesign to limit visibility of interal class details
travelmetrics를 Traveler 내부에서만 사용
이 방식도 꽤 괜찮지만 deinit side-effect를 제거하는 것이 제일 좋음
Redesign to avoid deinitializder side-effects
deinit에서 side-effect 발생하는 것을 제거해서
lifetime bug의 가능성을 제거하는 것이 좋음
(내 생각: 아래 제안하는 코드에서는 deinit에서 assert를 걸어줘서, Traveler 객체를 사용하는 쪽의 책임을 설명하고 있음)
Xcode 13 새기능
Optimize Object Lifetimes: Yes
(이거 알려주려고 차근차근 빌드업 한건가?!)
이렇게 지정해두면 객체가 마지막에 사용되는 지점에 보다 빠르게 deallocate 됨
object lifetime을 보다 일관성 있게 다룰 수 있는 장점이 있음
숨겨진 lifetime bug를 발견하게 될 수도 있음
정리
과외 선생님 마냥 개념 하나하나를 알려주는 세션이라서 유익했다.
메모리 관리는 디버깅 시에 이슈가 되는 경우가 많아서 개념을 잘 잡아두면 좋을 것 같다. (그래서 면접에도 단골 질문이다.)
애초에 순환 참조가 발생하지 않는 방식으로 설계해야겠다.
Xcode13 올리면 lifetime 최적화 옵션을 바로 적용해보고 싶다. 발견되는 이슈가 꽤 많을 듯.
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html 이 공식문서도 한번 읽어봐야겠다.
'iOS > Swift' 카테고리의 다른 글
[WWDC21] What's new in Swift (0) | 2021.07.10 |
---|---|
[WWDC21] Detect and diagnose memory issues (0) | 2021.07.07 |
[Swift 입문] 3. 변수와 상수 (0) | 2021.01.17 |
[Swift 입문] 2. Playground 사용해보기 (0) | 2021.01.17 |
[Swift 입문] 1. 우리가 Swift를 배워야하는 이유 (0) | 2021.01.17 |