iyOmSd/Title: Swift

[Swift] WWDC21 ARC in Swift: Basics and beyond

냄수 2021. 7. 24. 13:20
반응형

ARC에서 생길수 있는 객체생명주기에 따른 버그가 일어날 수 있는 상황을 알아보고 해결하는 방법을 제시하고

Xcode의 새로운 기술을 소개해주는 세션이에요

 

목차는 2개에요

Object lifetimes and ARC - 객체의 수명과 ARC

Observable object lifetimes - 객체수명에 따른 버그문제 해결방법

 

Class는 Swift에서 참조유형이며

사용을 결정하면 Swift는 자동으로 참조카운트 또는 ARC를 통해서 메모리를 관리해요

 

 

Object lifetimes and ARC

아래와같이 객체 수명과 ARC에 대해 설명합니다

  • 객체의 lifetime은 initialization에서 시작하고 마지막으로 사용할때 끝
  • ARC는 수명이 끝난후에 객체를 해제하여 메모리를 자동으로 관리
  • 레퍼런스 카운트를 추적해서 객체의 수명을 결정
  • ARC는 주로 스위프트 컴파일러에 의해 retain와 release를 삽입하는 작업을 구동
  • 런타임에 retain시 카운트를 증가하고 release시 감소
  • 런타임에 카운트가 0으로 떨어지면 객체가 해제

 

코드로 예를 들면

위 코드에서 traveler1은 Traveler객체의 첫번째 참조이고 마지막으로는 사본을 사용하죠

 

객체를 init할 때 retain

사용이 끝난 후 release를 한다면 

 

이렇게 release가 삽입될 것입니다

(init할때 카운트증가하므로 retain따로 삽입 x)

 

위 코드처럼 traveler2에 대한 참조계산은

참조하기전 retain

마지막으로 사용후 release가 추가될 것입니다.

 

 

위코드를 토대로

레퍼런스 카운트를 체크하는 과정을 자세히보면

init으로 카운트 1

 

Traveler객체에 대한 참조가 추가되므로

카운트 +1

-> 현재 참조카운트 2

 

이제 traveler2도 같은 객체를 참조함

 

traveler1 사용끝 

release로 카운트 -1

-> 현재 참조카운트 1

 

목적지 수정

 

traveler2 사용끝

release 카운트 -1

-> 현재 카운트 0

객체 할당해제

 

 

이런식으로 동작한다고 설명하고 있어요

 

Swift에서 객체의 수명은 사용기반이다

객체의 최소수명은 init에서 시작하며 마지막으로 사용하고 끝난다

 

하지만 문제가 생길 수 있다고 해요

사용 기반이라면 마지막으로 사용하고 객체를 할당해제 해줘야하는데

시점에서 문제가 있을 수 있죠

 

목적지 수정후 바로 할당해제 될 수도 있고

 

출력문실행 후 객체를 사용하는 곳을 넘어서 종료 될 수 있어요

 

 

 

Observable object lifetimes

weak and unowned references

deinitializer side-effects

위의 방법으로 객체의 수명을 관찰 할 수 있어요

수명에 의존하는 프로그램이 있다면 미래에 문제를 겪을 수 있어요

 

  • Relying on observed object lifetimes causes bugs
    • May remain hidden for long time
    • Maybe uncovered at surprising times
      • Compiler update
      • Source changes

지금은 정상적으로 효과가 있을 수 있지만 그것은 우연일 수도 있다고 해요 (위에서 언급했듯이 시점이 달라질 수 있음)

객체수명을 관찰하는 것은 Swift컴파일러 속성중 하나이고

컴파일러 구현 세부사항이 업데이트됨에 따라서 동작이 다르게 변경될 수 있어요

개발중 발견되지않고 오랫동안 숨겨져 있을 수도 있고

개선된 ARC최적화 또는 컴파일러 업데이트에 의해서 발견될 수 있죠

 

 

수명에만 의존한다면 일어날 수 있는 일을 살펴보고 그것을 고칠 수 있는 몇가지 기술을 검토해볼게요

 

Language features, Consequences and safe techniques

언어적 기능과 안전한 기술 알아볼거에요

 

언어적 기능으로 접근하면

많이들 알고있는 

weak, unowned를 이용하죠

 

레퍼런스 카운트를 계산하지 않고

순환참조를 깨는데 사용하죠

 

 

다시 코드로 예를 보여주네요

traveler는 account를 가지고있고 point를 축적 할 수 있습니다

Account클래스는 Traveler클래스를 참조하며

Traveler클래스는 다시 Account클래스를 참조해요

 

이제 위의 코드를보며

ARC작동 과정을 볼 거에요

 

Traveler객체가 생성되면서 카운트 1이 됩니다

 

Account객체를 만들면서 traveler를 참조하기 때문에

Traveler는 2 Account는 1이 됨

 

traveler가 account를 참조하므로

Account가 2가됨

 

account의 마지막 사용으로 카운트 1감소

Account가 1이 됨

 

traveler의 마지막 사용으로 카운트감소

Traveler가 1이됨

 

객체를 다 사용하고 모든 참조가 없어진 후에도

객체의 참조수는 1씩 남아 있는 것을 볼 수 있죠

이것은 순환참조 때문이에요

따라서 객체는 절대 해제되지 않고 메모리 누수가 일어나죠

 

이러한 문제를

weak와 unowned로 순환참조를 끊을 수 있고

이것들은 레퍼런스 카운트계산에 포함되지 않기 때문에 참조가 사용되는 동안

참조되는 객체를 해제 할 수 있죠

이러한 일이 일어난다면 Swift런타임이 weak에 대한 접근은 nil로 안전하고

unowned에 대한 접근은 trap이 되죠

 

위의 코드를 weak로 변경하면

Traveler객체를 마지막으로 상용한 후

참조카운트가 0으로 떨어지기 때문에

할당 해제되겠네요

 

또한 Traveler객체가 사라져서

Account에 대한 참조도 감소해서 0이되고

Account객체도 할당해제 되네요

 

 

약한 참조를 사용하여 객체의 수명이 종료되는 동안

수명에 의존하고 있고 객체에 접근하고 있는 경우 미래에 버그가 나타날 수 있는 문제점이 존재하죠

 

 

코드를 아래와같이 변경했다면

(print를 Account로 옮기고 test함수 변경)

변경된 코드

Traveler의 마지막 사용은

printSummary()를 호출하기 전이에요

컴파일러가 release를 삽입하여 Traveler의 레퍼런스 카운트가 0이 되고

약한참조를 통해 할당해제 될 수 있어요

 

이떄 account.printSummary()를 호출하면

여행자의 이름과 포인트를 출력할 수도 있지만

이출력은 우연일 수도 있어요

 

nil이기 때문에 충돌을 일으킬 수도 있죠

 

nil을 해결하기위해 아마도 옵셔널 바인딩을 사용하겠죠?

 

옵셔널 바인딩은 실제로 문제를 악화시켜요

명백한 충돌없이 조용한 버그를 만들기 때문에

객체의 수명 관찰이 변경될 때 눈에 뜨지않죠

(위에서 말한 컴파일러의 변경이나 우연으로 변경 됬을때 버그이유를 찾기어려움)

 

이러한 weak, unowned참조를 안전하게 처리할 수 있는 기술이 있어요

withExtendedLifetime()를 사용하고

강한참조를 이용한 접근으로 재설계하고

weak, unowned참조를 피하도록 설계하는 것이죠

 

withExtendedLifetime

스위프트는 객체의 수명을 명시적으로 연장할 수 있는 withExtendedLifetime()를 제공해요

Traveler객체의 수명을 안전하게 연장할 수 있고 printSummary()호출하여 잠재적인 버그를 예방할 수 있어요

 

또는 기존 scope끝에 withExtendedLifetime을 빈 호출로 사용해서 동일한 효과를 얻을 수 있어요

 

또는 defer을 사용하여 현재 scope의 끝까지 객체의 수명을 연장 할 수 있어요

 

withExtendedLifetime()은 객체수명과 관련된 버그를 쉽게 해결 할 수있는 방법으로 보일 수 있어요

하지만 이 기술은 취약하고 당신에게 정확성의 책임을 전가하죠

 

이 방법을 사용하면 weak가 버그를 일으킬 가능성이 있을 때마다 withExtendedLifetime를 사용해야하고

통제되지 않으면 withExtendedLifetime이 코드베이스 전체에 걸쳐 서서히 증가하여 유지보수 비용이 증가하게되요

따라서 더 나은API로 클래스를 재설계하는 것이 훨씬 더 원칙적인 접근 방식이라고 하네요

 

강한참조 접근으로 재설계

객체에 대한 액세스를 강한참조로만 제한할 수 있는 경우 객체수명문제를 방지 할 수 있어요

여기서 printSummary()함수를 다시 Traveler클래스로 이동하고 Account클래스의 약한참조를 private처리 했습니다.

이제 강한참조를 통해 printSummary()를 호출하여 잠재적인 버그를 제거할 수밖에 없죠

성능비용을 부담하는 것 외에도 클래스 설계에 주의하지 않으면 weak, unowned참조가 버그를 야기 시킬 수 있음

 

weak,unowned 참조를 회피하도록 재설계

왜 weak, unowned참조가 필요한가?를 잠시 멈추고 생각하는 것이 중요하다고 하네요

그것들은 단지 참조순환을 깨기위해 사용되는가?

애초에 순환참조를 만드는것을 회피하려면?

순환참조는 알고리즘을 다시 생각하고 순환클래스관계를 트리구조로 변환함으로써 피할 수 있어요

(애초에 순환참조가 생기지않도록 설계하자! 라는 내용이에요)

 

Traveler클래스가 Account클래스를 참조해야하고

Account클래스가 Traveler클래스를 참조할 필요는 없다

Account클래스는 여행자의 개인 정보에만 액세스하면 돼죠

그렇다면 여행자 개인정보를 PersonalInfo라는 새로운 클래스로 옮길 수 있겠네요

Traveler클래스와 Account클래스는 모두 PersonalInfo클래스를 참조할 수 있으며 순환을 피할 수 있게돼요

weak, unowned참조의 필요성을 회피하는 것은 추가비용이 들 수 있지만

이것은 객체수명 버그를 예방할 수 있는 확실한 방법이다.

 

객체수명을 관찰할 수 있는 또 다른 방법중 하나는 deinitializer side-effects가 있죠

할당해제전에 실행되며 side effect로 외부프로그램에 영향을 줄 수 있어요

숨겨진 버그로 이어질 수 있으며 상관없는 이유로 관찰된 객체 수명이 변경될 때 발견 될 수 있다.
(which are uncovered only when the observed object lifetime changes due to unrelated reasons.)

 

 

deinit은 콘솔에 출력 메세지를 찍는 global side effect를 가지고 있어요

 

오늘날 이 deinit은 "Done traveling"을 출력후 실행 될 수 있어요

 

그러나 객체의 마지막 사용은 destination업데이트 이기때문에,

ARC최적화에 따라서 "Done traveling"이 인쇄되기 전에 deinit을 실행할 수 있어요

 

deinitializer side effects were observable but not relied upon.

이 예에서는 deinit side effect를 관찰할 수 있었지만 의존하지 않았음

 

 

더 복잡한 예를 들어보자면

where deinitializer side effects are relied upon by external program effects.

아래의 예에서는 deinit side effect가 외부 프로그램의 영향에 의존한다

목적지가 업데이트되면 TravelMetrics클래스에 기록해요

 

deinit이 호출되면 global로 published가 실행

published는 여행자의 익명ID, 찾는 목적지 수, 관심여행 카테고리로 이루어져있어요

 

테스트함수에서 Traveler객체가 생성되고

TravelMetrics에 대한 참조가 복사되요

여행자의 목적지가 "Big Sur"로 업데이트 되고

다시 목적지가 "Catalina"로 업데이트 되고

목적지 기록을 바탕으로 관심여행 카테고리를 계산해요

 

deinit은 computeTravelInterest()실행 후 작동하고,

관심 카테고리를 Nature로 publishing할 거에요

 

그러나

Traveler객체의 마지막사용은 "Catalina" 목적지 업데이트인데

그 직후 deinit이 실행 될 수도 있죠

 

deinit실행 이후 computeTravelInterest()가 실행되기 떄문에

nil을 published하게되고

결국 버그를 야기하게 돼죠

 

이러한 deinit side effect를 안전하게 처리할 수 있는 기술로

withExtendedLifetime()

내부클래스 정보에 가시성을 제한하는 재설계

deinitializer side-effects를 피하도록 재설계

가 있어요

각 단계는 다양한 수준의 사전 구현 비용과 지속적인 유지보수 비용을 가지고 있어요

 

withExtendedLifetime

withExtendedLifetime()은 computeTravelInterest()가 실행될때까지

Traveler객체의 수명을 명시적으로 연장하여 잠재적인 버그를 예방하는데 사용할 수 있어요

이것은 앞서말했듯 정확성에 대한 책임을 당신에게 전가시키죠

이 접근법을 사용하면 deinit side effect와 외부프로그램 영향 사이에 잘못된 상호작용이 있을 때 마다 사용해야하고

유지보수비용을 증가시켜요

 

 

내부클래스 정보에 가시성 제한을 통한 재설계

deinit side effect가 모두 local에 영향을 주는경우 관찰할 수 없어요

내부클래스 정보에 가시성을 제한하여 클래스API를 재설계하면 객체수명버그를 방지할 수 있죠

위의 코드에서 TravelMetrics는 외부접근으로부터 숨겼고 private처리 돼있어요

deinit은 computeTravelInterest()를 하고 publish()한다

이 방법은 효과가 있지만 더 원칙적인 접근방식은 deint side effect를 완전히 없애는 것이에요

 

deinit side effect를 회피하도록 재설계

deinit대신에 defer을 사용하여 publish를 하고

deinit은 확인만 수행하도록 구현했어요

deinit side effect를 제거함으로써 모든 잠재적인 객체수명 버그를 제거할 수 있죠

 

 

Xcode13을 사용하면 Swift컴파일러에서 build setting에 "Optimize Object Lifetimes"라는 새로운 경험을

설정해서 사용할 수있다고해요

이를 통해 강력한 수명 단축 ARC최적화를 가능하게 해줘요

이 설정을 키면 마지막으로 사용한 직후에 객체가 훨씬 더 일관되게 할당해제되는걸 볼 수 있고

객채수명을 보장된 최소 수명(객체의 최소수명은 init에서 시작하며 마지막으로 사용하고 끝난다)에 가깝게 만들어 줍니다.

 

이 설정을 사용해도 위에서 논의된 예와 유사한 객체수명버그를 야기할 수도 있어요

그럴땐 이 세션에서 논의된 안전한 기술을 따라 해당 버그를 모두 제거해서 사용하라고 하네요!

 

 

 

반응형