Red(레드)
RED BITES APPLE
Red(레드)
전체 방문자
오늘
어제
  • 분류 전체보기 (22)
    • 🍎 iOS (20)
      • 🕊️ swift (9)
      • ⌨️ xcode (1)
      • 🍏 UIKit (7)
      • 🍑 SwiftUI (0)
      • 🧑🏻‍💻 WWDC (3)
    • 🖥️ CS (1)
    • 📘 용어 사전 (1)
    • TIL (0)

블로그 메뉴

  • POST
  • Git Hub

공지사항

인기 글

태그

  • til
  • meta

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Red(레드)

RED BITES APPLE

[WWDC] ARC in Swift: Basics and beyond
🍎 iOS/🧑🏻‍💻 WWDC

[WWDC] ARC in Swift: Basics and beyond

2023. 1. 6. 01:33

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")
}
  1. test 메서드 안에서 traveler1 을 만들었다. 이 시점에 초기화가 되고 RC 는 1이 된다.
  2. traveler2에 traveler1 을 할당했다. 할당이 일어났으니 RC 를 1 늘리는 작업이 필요하다. 컴파일러가 런타임에 retian 을 넣어주어 RC가 1이 잘 증가했다.
  3. 그 라인 이후로 메서드가 끝날 때까지 traveler1이 사용되는 일은 없다. 컴파일러는 그래서 release 를 호출해주어서 RC를 하나 줄여 준다.
  4. traveler2.destination 의 값을 변경했다. 이 이후에 traveler2 도 사용할 일이 없다. 그래서 컴파일러가 자동으로 release 를 해주었다.
  5. 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 을 남발하지 말고 꼭 써야 할 곳에만 쓰자! 구조 자체를 수정해서 유지 보수를 쉽게 하자!


참고자료
WWDC: ARC in Swift: Basics and beyond

'🍎 iOS > 🧑🏻‍💻 WWDC' 카테고리의 다른 글

[WWDC19] SwiftUI Essentials  (0) 2023.01.10
[WWDC] Architecting Your App for Multiple Windows  (0) 2023.01.06

    티스토리툴바