cozzin
코찐 기술 블로그
cozzin
전체 방문자
오늘
어제
  • 분류 전체보기
    • Kotlin
    • 백엔드
    • iOS
      • Swift
      • SwiftUI
      • Combine
      • Architecture
    • 개발환경
    • 세미나
    • 생각정리
    • 스터디
    • CS
      • Refactoring
      • OS

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • LinkedIn
  • 강의

공지사항

인기 글

태그

  • darkmode
  • 운영체제
  • 워닝제거
  • WWDC21
  • 컴퓨터공학
  • os
  • WWDC
  • multicast
  • 테스트
  • 리팩토링
  • Combine
  • ios
  • SwiftUI
  • CS
  • Ribs
  • slide-over
  • XCode
  • Swift
  • 디자인패턴
  • Warning

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
cozzin

코찐 기술 블로그

[WWDC21] ARC in Swift: Basics and beyond
iOS/Swift

[WWDC21] ARC in Swift: Basics and beyond

2021. 7. 4. 23:44
반응형

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()

Apple Developer Documentation

(이런것도 있었구나...)

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
    'iOS/Swift' 카테고리의 다른 글
    • [WWDC21] What's new in Swift
    • [WWDC21] Detect and diagnose memory issues
    • [Swift 입문] 3. 변수와 상수
    • [Swift 입문] 2. Playground 사용해보기
    cozzin
    cozzin
    Software Engineer

    티스토리툴바