nyancoder

WWDC 2021 - ARC in Swift: Basics and beyond 본문

WWDC/WWDC 2021

WWDC 2021 - ARC in Swift: Basics and beyond

nyancoder 2021. 7. 4. 19:46

원본 영상: https://developer.apple.com/videos/play/wwdc2021/10216/

 

Object lifetimes and ARC

  • 객체의 수명은 init()에서 시작하여 마지막 접근 이후 끝납니다.
  • ARC는 수명이 끝난 객체의 메모리를 자동 해제합니다.
  • ARC는 참조 횟수를 추적하여 개체의 수명을 결정합니다.
  • Swift 컴파일러는 ARC를 위해 retain/release 연산을 추가합니다.
  • 참조 횟수가 0으로 떨어진 객체는 할당 해제됩니다.

  • Traveler 객체는 생성되었을 때 ref_count가 1로 만들어집니다.
  • traveler1에서 traveler2에 객체가 할당되었을 때, retain이 불리며 ref_count가 1 증가하여 2가 됩니다.
  • 이후 traveler1은 더 이상 참조되지 않으므로 release가 호출되면서 ref_count가 1 감소하여 1이 됩니다.
  • traveler2도 더 이상 참조되지 않게 되면 release가 호출되면서 ref_count가 1 감소하여 0이 됩니다.

  • 컴파일러에 의해 추가된 release연산에 의해 ref_count가 0이 되면 오브젝트가 해제됩니다.

  • 최적화 옵션에 의해서 release연산이 마지막 사용 이후에 있을 수 있으며 이때에는 더 늦게 해제될 수 있습니다.

  • weak나 unowned 참조나 deinit과 같은 기능을 통해서 객체의 수명을 확인할 수 있습니다.

  • 객체의 수명에 의존하는 코드는 버그를 만들 수 있습니다.
  • 이러한 버그는 오랜 기간 동안 발견되지 않을 수 있으며, 컴파일러의 업데이트나 컴파일러의 최적화와 연관된 다른 부분의 소스의 변경에도 영향을 받을 수 있습니다.

 

Observable object lifetime

  • weak나 unowned 참조는 참조 횟수를 세는데 영향을 주지 않기 때문에, 주로 순환 참조를 막는 데 사용됩니다.

  • 순환 참조란, Traveler.account가 가리키는 Account의 Account.traveler가 다시 처음의 Traveler를 가리키는 것과 같은 현상을 말합니다.
  • 이 경우에는 test함수가 종료되어도 각각의 객체의 ref_count가 1 미만으로 내려가지 않기 때문에 객체가 해제되지 않습니다.

  • weak나 unowned 참조는 참조 횟수를 세는데 영향을 주지 않기 때문에, 순환 참조를 막을 수 있습니다.
  • 대신 참조하고 있는 객체가 해제될 수 있기 때문에 해제된 객체에 대해 접근할 때에 대한 별도의 처리가 있습니다.
  • weak 참조는 대상이 해제되면 nil이 되도록 처리되어 있습니다.
  • unowned 참조는 대상이 해제된 상태에서 접근하는 것을 탐지하여 오류를 발생합니다.

위의 예제에서는 Account의 traveler를 weak 참조로 변경하여 프로그램이 종료될 때 traveler가 해제될 수 있습니다.

 

  • account.printSummary()를 호출하면 traveler의 마치 막 참조 이후에 traveler를 참조하는 것이기 때문에 오류가 발생할 수 있습니다.
  • 오류를 막기 위해 printSummary() 함수 내에서 nil 체크를 수행할 수 있지만, 크래시만 발생하지 않을 뿐 여전히 버그로 남을 수 있습니다.

  • withExtendedLifetime() 함수를 사용하면 명시적으로 객체의 수명을 특정 시점까지 연장할 수 있습니다.
  • withExtendedLifetime의 몸체를 비워두어도 동일한 효과를 얻을 수 있습니다.
  • 또는 보다 명시적으로 함수의 끝까지 객체를 유지하기 위해 defer문 안에서 사용할 수 있습니다.
  • 하지만 이런 방법은 약한 참조가 버그를 일으킬 가능성이 있을 때마다 withExtendedLifetime을 적절히 배치해야 하므로 관리 비용이 증가합니다.

  • 약한 참조에 대한 접근은 private으로 제한하고, 외부에서 접근해야 하는 경우에는 strong 참조를 통해서만 참조하면 문제를 피할 수 있습니다.

  • 약한 참조는 생명주기에 의존하기 때문에, 아예 약한 참조를 사용하지 않는 것이 가장 좋은 방법입니다.
  • 기존 구조를 수정하여 더 이상 순환 참조를 하지 않도록 디자인하면 모든 생명주기에 관련된 문제가 해결됩니다.

  • deinit은 할당이 해제되기 직전에 호출됩니다.
  • deinit에 side-effect가 있는 코드를 작성한 경우에는 객체의 수명이 변경될 때 발생하는 버그가 생길 수 있습니다.

  • 예상되는 결과는 "Lily is deinitializing" 출력 후 "Done traveling"이 출력되는 것입니다.
  • ARC 최적화에 따라서 "Done traveling" 출력 후 "Lily is deinitializing" 이 출력될 수 있습니다.

  • computeTravelInterest() 함수가 category를 계산하기 때문에, traveler의 소멸이 computeTravelInterest() 이후인 경우에는 category가 포함되어 전송될 것이라고 예측할 수 있습니다.
  • 하지만 traveler객체의 마지막 사용이 computeTravelInterest()를 호출하기 전이기 때문에 category가 계산되지 않고 nil로 처리될 수 있습니다.

  • 이러한 경우 withExtendedLifetime를 호출하여 명시적으로 traveler객체의 수명을 연장할 수 있습니다.
  • 하지만 잘못된 객체 수명을 참조하는 코드가 있을 때마다 호출해주어야 하므로 관리 비용이 늘어납니다.

  • travelMetrics 객체를 private으로 선언하여 외부에서 computeTravelInterest()를 호출하지 못하게 하는 방법이 있습니다.
  • 이런 경우 항상 deinit내에서 publish() 호출 이전에 computeTravelInterest()를 호출하게 되므로 문제가 해결됩니다.

  • 근본적인 해결 방법은 deinit에서 side effect가 있는 코드를 호출하지 않는 것입니다.
  • side effect가 있는 코드는 deinit 외부에서 처리하고, deinit에서는 수행에 대한 검증만을 수행하여 문제를 해결할 수 있습니다.

  • Xcode 13에서는 Optimize Object Lifetimes라는 실험적인 빌드 설정을 할 수 있습니다.
  • 이 빌드 설정을 활성화하면 객체가 수명이 좀 더 마지막 사용 직후에 일관되게 끝나는 것을 볼 수 있습니다.
  • 이러한 옵션의 사용은 숨겨진 객체의 수명과 관련된 버그를 드러나게 해 줍니다.

 

 

목차: https://nyancoder.tistory.com/2

Comments