안녕하세요:)
오랜만에 WWDC아티클로 찾아온 킴스캐슬입니다
최근에 예전에 한번씩 봤던 WWDC영상을 한번더 보면서 아티클을 작성해보려하고있습니다
이유야 여러가지가있겠지만 우선 처음에 WWDC영상을 봤을때는 10중에 2정도만 이해가되고 나머지는 이해가안됐던 부분이 많았었는데 아무래도 시간이지나면서 정량적, 정성적으로 실력이나 경험이 쌓이면서 이해할수있는 능력치가 올라갔다고 생각합니다. 그런김에 정리를 하면 내 지식으로 만들수있을거라는 확신도 있고요
막상 회사에 들어가니 기본적인 기틀이 마련되있는 프로젝트를 하면서 내가 기여할 수 있는 부분에 대한 아이디어를 찾는데는 WWDC만한게 없더라고요 물론 오늘 ARC는 프로젝트에 기여할수있는 부분은 아니지만 앞으로swift macro라던지 iOS버전이 아닌 swift버전만 충분하다면 적용할 수있는 신기술에 대한 아티클도 작성해보려합니다
오늘의 WWDC영상의 메인주제는 Automatic Reference Counting이고 ARC라고 부르는게 더 익숙한 Swift의 메모리관리에 대한 이야기 입니다
다른블로그글을 보면서 참고한 내용도 많지만 단순히 단어자체를 번역해서 설명한 글도 많다보니 단어하나하나가 어떤의미인가를 설명하는 나름(?) 자세한 설명들도 꼼꼼히 적어보도록하겠습니다
(스압주의!)

swift에서는 구조체(struct)와 열거형(enum)같은 아주 강력한 value type(값유형)을 제공합니다
apple에서는 정말 reference type(class, actor등 heap에 저장되는 객체들)이 필요한 상황이 아니면 reference type이 아니라 value type을 사용하기를 권장합니다
왜냐면 reference type을 사용하게되면 의도하지않은 객체내 property가 공유(share)되어서 의도하지않은 효과(side effect)를 유발할수있고 이게 앱을 구성할때 치명적인 결합이될수있기 때문이죠. 애초에 swift의 성능에 대한 WWDC영상인 understanding swift performance에서도 value type이 빨라서 성능상이점이있다고 이야기를 하니까요
Dangers of unintended sharing(의도하지않은 공유의 위험)?
class A { var num = 1 } let a = A() let b = a a.num = 3 print(a.num) // "3" print(b.num) // "3"위의 코드와같이 객체를 reference type인 class로 만들면 b라는 변수가 a라는 객체의 주소값을 복사해서 바라보기때문에 a의 num을 3으로 바꾸면 b또한 똑같인 A라는 Class를 바라보고있기에 3이라는 값을 출력하게됩니다. 공유되지않는 value type인 struct로 A를 만들었다면 a가 num을 바꾸더라도 b의 num은 1로 그대로 남아있을겁니다. 이러한 현상을 reference type이 가지는 특징이고 이로인한 공유로인해 의도하지않은 상황이 발생할수있음을 말해주는 내용입니다
게다가 swiftUI로의 패러다임전환에서도 중요한 요소중하나로 swiftUI에서의 view는 struct이다도 이 같은 맥락으로 해석이 가능하기도 합니다
class는 위의 예시처럼 대표적인 reference type이고 class를 사용하기로 결정한 순간 swift는 자동으로 Automatic Reference Counting(이하 ARC)를 통해 메모리를 관리하게 됩니다
Value type의 reference Count?
reference count를 반환해주는 함수가있는데 rc값을 반환해주기위한 객체를 인자로 넘겨야합니다
근데 공식문서를 보면 인자로 CFTypeRef타입의 객체를 넘겨야하는데 해당 타입은 AnyObject의 typealias이기에 클래스타입이어야합니다따라서
value type의 reference count는 없다!보다는 아얘 서로 상관없는 개념이다라는게 구현된 메서드에서 추론가능합니다
하지만 우리가 늘, 항상 value type을 사용할 수는 없죠. UIkit만해도 맨날쓰는 ViewController는 class잖아요. 따라서 reference type을 우리는 어쩔수없이 사용해야하고 이때 ARC가 이용되기때문에 우리는 ARC의 작동방식을 이해하고 있어야합니다(우리는 개발자니까요)
ARC vs GC(Garbage Collection)
이건 제가 실제 면접때 GC와 ARC의 차이에대해 질문을 받은적이있어서 정리하고 넘어가보려합니다
python은 대표적으로 GC를 쓰는 언어중 하나이고 ARC를 사용하는 언어인 swift를 저희는 사용합니다
실제로 python을 사용할때 swift였다면 강한순환참조가 발생하는 코드가 있어서 확인해보니 그런 상황일떄 메모리가 알아서 해제를시키는걸보고 신기했던 경험이 있습니다
이제 본격적으로 ARC에대한 WWDC의 내용이 시작되는 챕터가 시작됩니다. 여기서는 Object lifetimes라는 객체의 수명이라는 개념과 observable object lifetime이라는 관찰가능한객체의 수명이라는 개념이 나오게되는데요 한번 어떤 개념인지 차근차근 알아봅시다

먼저 객체의수명에 대한 설명을 보겠습니다
첫번째줄을 보면 객체의 수명은 초기화시 시작되고 마지막으로 사용될때 끝난다고합니다
두번째줄을 보면 객체의 수명이 끝나면 ARC가 그 객체를 메모리에서 해제(deallocated)한다고하네요
세번째줄을 보면 ARC는 객체의 수명을 reference count와함께 추적한다고합니다
네번째줄을 보면 swift compiler는 retain과 release라는 operator를 삽입한다고하네요
마지막줄을 보면 reference count가 0이되면 해당 객체는 메모리에서 해제된다고 합니다
그냥 해석해보면 뭔가 알듯말듯하네요 예시를 보면서 어떤 내용인지 자세히 보죠

이런 코드가 있다고 칩시다
Traveler라는 class객체가 있죠
첫번째 설명을 보면 객체의 수명은 초기화할때 시작되고 마지막으로 사용될때 끝난다라고 하는데 그러면 traveler1이라는 객체는 언제 시작되고 언제끝날까요?

순수하게 traveler1이라는 객체는 생성되고 traveler2라는 객체에 할당될때 마지막으로 사용됩니다
객체 수명에 대한 설명 세번째줄을 보면 ARC는 객체의 수명을 reference count와 함께 추적하고 네번째줄을 보면 retain과 release라는 operator를 삽입한다고 했죠
이 두가지 개념이 함께 사용됩니다. 우선 객체의 참조값이 올라갈때 compiler는 retain이라는 operator를 삽입해주고 마지막으로 사용될때는 release라는 operator를 삽입해줍니다. 그리고 retain은 reference count를 1늘려주고 release는 reference count를 1감소시켜줍니다
결국 Traveler라는 class의 객체는 retain으로 인해 reference count가 1증가되고 release로 인해 reference count가 1감소하게됩니다
우선 위 코드에서 retain과 release라는 operator가 어느시점에 insert되는지를 확인해보겠습니다 우선 traveler1의 관점에서 볼까요?

어? 왜 객체가 생성되는 init시점에 retain이 insert되지 않나요???
retain은 결국은 reference count를 1증가시켜야댐!이라고 말하는 표시인데 객체가 생성될때 무조건 reference count가 1로 초기화가 되기때문에 객체 생성시점에는 retain이 insert되지않는다고합니다
결국 traveler1이라는 객체는 traveler2에 넣어주는순간이 마지막으로 사용되는 시점이기에 여기에 release operator가 insert되게됩니다

traveler2는 어떤가요. 객체의 reference를 참조하는 시점에서 retain operator가 insert되고 마지막으로 사용되는시점(destinatrion을 다른값으로 할당)에 release가 insert됩니다

실제 heap에 올라가있는 Traveler라는 객체의 입장에서 순서대로 코드를 보면 우선 객체가 생성되어서 traveler1이라는 변수에 할당되면 reference count가 1로 초기화가 됩니다. 그리고 retain을 만나게됩니다. retain을 만나면 reference count가 1증가해서 2가되겠죠. 그리고 동시에 traveler1이란 객체가 마지막으로 사용되어 release를 만나 reference count가 1감소해 1이되고 마지막으로 traveler2가 사용되는순간 release를 만나서 reference count가 0이됩니다
결국 reference count가 0이되면 해당 객체는 메모리에서 해제된다고 합니다에서의 설명처럼 reference count가 0이되어 메모리에서 deallocation되게 됩니다
swift에서의 object lifetime(객체수명)은 use-based라고합니다. 사용기반?이라고 해석을 하는데 객체의 수명이 test라는 함수가 끝나는시점에 기반한것이 아닌 정말 사용이 끝났는지에 기반해서 수명을 결정짓는다는 뜻같습니다
그리고 여기서 이런 객체수명(순수하게 생성~마지막사용까지의 수명)을 guaranteed minimum lifetime이라고 이야기합니다. 해석하면 보장된 최소 수명정도로 볼 수 있겠네요
C++같은 언어는 메서드가 끝나는 시점인 닫는 중괄호에서 객체가 해제되지만 swift에서는 순수하게 객체가 언제 마지막으로 사용되는지가 수명을 결정짓는다고합니다

결국 위에서봤던 retain/release operator를 통한 reference count가 0되는시점에 객체가 해제됩니다
위 코드에서는 "Done traveling"이 출력되기전에 객체가 메모리에서 해제될겁니다
하지만 실제 객체수명은 컴파일러에 의해 삽입된 retain/release operator의 작업에의해 결정됩니다. 이게 무슨말이냐면 실제로 release operator의 수행이 끝나야 메모리에서 해제가되게됩니다.
같은말아닌가할수있지만 실제로 객체가 마지막으로 사용되는시점과 그 시점에 insert된 release operator를 처리하고 끝나는 시점이 다를수있다는겁니다. 그래서 위에서 보장된 최소 수명라는 단어로 표현을 한것같습니다. 실제로는 마지막으로 사용되는순간 객체가 해제되어야하는데 operator를 처리하는시간때문에 아래와같이 객체의 마지막사용시점 이후에 메모리의 할당해제가 일어날수도 있습니다

그리고 이런경우가 발생하면 print문 이후에 객체가 메모리에서 할당해제됩니다
간단히 정리를해보면 실제로는(개념상으로는) 객체가 마지막으로 사용되는 그 시점에 메모리에서 할당해제가 되는 보장된 최소수명으로 동작해야 맞으나 실제 ARC의 최적화에따라서 observed object lifetime(이상적인 수명이 아닌 실제로 행해지는 수명)으로 동작할수있다는겁니다
뭐...늘 그렇죠 이상과 현실의 괴리감같은거라고 생각하면 좋지않을까싶네요

우리가 reference count를 계산할때 순수하게 끝나는시점에 reference count를 1줄여서 여기서 0이되니까 메모리가 해제되겠네!라고 생각해서 뒤 로직을 짰을때 현실적으로는 observed object lifetime(이상적인 수명이 아닌 실제로 행해지는 수명)때문에 우리가 생각치못하게 동작할수도있다는겁니다.
아뉘...근데 우리가 파악하고 계산할수있는건
보장된 최소수명인데 ARC가 어떻게 동작할줄알고observed object lifetime를 고려하나요... 오히려observed object lifetime가 어떻게 동작할지는 아무도 모르는거아닌가요...?
맞습니다 우리가 observed object lifetime에 의존하게된다면 문제가발생할수있습니다(예상하지못하니까요) 현실적으로는 observed object lifetime에 의존해야하지만 우연에 기대는거일수도있죠. observed object lifetime는 swift의 컴파일러의 세부사항에따라 변경될수있거든요.
즉 observed object lifetime는 우리가 컨트롤못하고 예측못하는 변수인거고 보장된 최소수명인 guaranteed minimum lifetime는 우리가 예측할 수 있는 상수인겁니다
우리가 실제로 예측을 못하는데 아...뭐...실제로 여기서 해제안된다고했으니까 그다음 코드에서 접근해도 괜찮긴하겠지하고 접근하면 무슨문제가 발생할지모르니까 우리는 안전하게 이론상으로는 여기서 해제되지만 안전하게 여기서 해제안되게 만드는 방법이있을까?의 생각을 해볼수있는거죠 그리고 그 방법을 포함해서 Observable object lifetimes를 고려하지 않는 방법들에대해 알아볼겁니다!

위 코드의 test메서드가 실행되면 어떻게 될까요 
release operator를 통해 reference count가 감소된 이후에 객체간의 reference count가 서로 0이 되지않아 메모리에서 해제되지않는 메모리누수가 발생하게됩니다
보통 이런경우에 weak이나 unowned를 사용해서 reference count를 증가시키지않게해서 이런 강한순환참조를 예방하고 메모리누수가 발생하지 않게끔합니다

설명을 덧붙이자면 weak의 경우엔 nil을 바라보게할수있고 만약에 unowned키워드에 접근했을때 trap이 발생할수있습니다(reference count를 증가시키지는 않지만 객체를 바라보고는 있기에 이런 상황이 발생합니다)
코드를 약간 수정해보겠습니다

print함수가 Account객체로 이동되었고 여전히 발생할수있는 강한순환참조때문에 weak을 사용했습니다. 그렇게되면 결국 마지막코드에서 print함수가 호출됩니다
실제 위의 코드를 실행시키면 어떻게될까요?
아마도 의도한대로 traveler의 이름과 point가 잘 출력될겁니다

아마도 잘 작동되는게 우연일뿐일겁니다
왜냐면 위의 코드에서 traveler의 마지막사용시점에서 Traveler객체의 reference count가 0이되겠죠. Account는 traveler객체를 weak으로 들고있으니까요

그러면 이론상으로는(보장된 최소수명기준으로는 이라고 표현할수있겠네요) traveler가 마지막으로 사용된 순간에 Traveler라는 객체는 할당해제가 되어야하는거고 account가 print를 할때는 traveler객체는 nil이어야합니다. 당연히 강제언래핑을 했으니 오류가발생해야합니다
만약에 옵셔널바인딩을 했다면 출력이안될거고 대체 어디서 오류가 발생했길래 출력이안되는거야하며 멀쩡한 코드를 보며 여긴가? 하면서 시간을 낭비할수도있습니다. 이런경우 옵셔널 바인딩은 문제를 악화시킵니다. 그리고 이런버그를 영상에서는 silent bug라고 합니다
결국 하고자하는 이야기는 명확합니다. 우리는 보장된 최소수명을 기준으로 동작하는 코드를 짜야합니다. 보장된 최소수명를 고려하지 않았지만 동작하는 코드는 언제 어디서 문제가 발생할지모르는 시한폭탄이라고 할수있습니다. 생각해보면 더 무서운게 왜 발생했는지도 찾기어려울수있겠네요(by 옵셔널 바인딩)
자 그래서 이번 챕터에서는 weak과 unowned를 사용할때 보장된 최소수명을 기준으로 동작하는 코드를 작성하는 방법을 배우게됩니다

swift에는 객체의 수명을 명시적으로 연장시킬수있는 메서드가 존재합니다 메서드의 인자로 연장시키고싶은(해당 메서드의 실행시점까지) 객체를 넣어주면 해당메서드가 실행될때까지 parameter로 전달된 객체의 deallocation시점을 지연시킵니다
원래 코드에서 문제가 발생할수있었던 이유는 traveler라는 객체의 마지막사용시점이후에 보장된 최소수명이 아닌 observed object lifetime에 의존하는 코드였었기 때문이었습니다(그냥 운이 좋았던거죠)

이런상황에서 우리가 print문을 문제없이 사용하려면 traveler라는 객체가 해당 print문을 출력하는 메서드의 사용시점까지 객체수명이 연장되어야합니다. 아래와같이 사용하면됩니다

꼭 이렇게하지않고 빈클로저로도 똑같은 효과를 낼 수 있습니다(defer에 넣어도됌)

이 방식은 보장된 최소수명에 의존하기때문에 잠재먹인 버그를 방지할수있고 쉬운방법처럼 보이지만 그렇게 좋은 방법은 아닙니다. 왜냐면 개발자가 하나하나 가능성을 확인하고 넣어줘야하고(영상에서는 정확성에 대한 책임이 you에게 전가된다라고 표현하네요) 이 방식을 사용하면 weak나 unowned가 버그를 일으킬 가능성이 있을때마다 사용해야합니다. 한땀한땀말이죠...이방식이 코드전체에 퍼져있다면 유지보수비용이 늘어납니다. 길게봤을때 좋은 방식이라고 말할수없죠
다시 print메서드가 Traveler에 있는 코드로 돌아가봅시다
원래 코드에서 언제문제가 발생할수있는지를 보면(좀 억지스럽긴하지만)
traveler의 print메서드에 접근하기전에 Account에 weak으로 선언되어있는 traveler변수에 강제로nil을 할당한다면 혹은 접근해서 할당된다면 문제가 발생할겁니다
결국 weak으로 선언된 변수는 private으로 선언해준다면 이런 문제를 방지할 수 있습니다

사실 이게 엄청 실용적인 예제인지는 모르겠습니다...실제 영상에서 weak 변수를 private으로 선언해서 잠재적 문제를 예방하라고하는데 실제 개발할때 private을 하지않아서 변수에 강제로 nil이 할당되는바람에 접근하다 문제가 발생했던 적이 없어서 그냥 그러려니하고 넘어갔던 부분입니다 ㅎㅎ...
weak과 unowned가 필요한 이유가 무엇인가요? 강한순환참조를 예방하는데 사용되죠. 그렇다면 애초에 강한순환참조를 발생하지 않게 설계한다면 weak와 unowned로 인한 잠재적버그를 예방할수있습니다. 알고리즘을 다시생각하고 순환관계의 객체구조를 트리구조로 변경한다면 강한순환참조를 피할수있는 경우가 많습니다
만약에 지금까지 weak를 써서 강한순환참조를예방했던 구조자체를 객체가 서로를 바라보지않는 트리구조로 바꾼다면 어떻게될까요

기존의 방식은 위 그림처럼 Traveler라는 객체와 Account라는 객체가 서로를 참조하는바람에 weak가 필요했습니다

근데 애초에 객체구조를 위 그림처럼 바꾼다면 객체간의 서로를 참조하는 구조가 아니기때문에 강한순환참조가 발생하지않아 weak를 사용할필요가없고 observed object lifetime로 인한 잠재적 버그를 걱정할 필요가 없어집니다
이번에는 deinit시점에 대한 이야기입니다
지금까지의 이야기를 생각해보면 분명 deinit되는 시점은 보장된 최소수명에 의하면 마지막사용시점이겠지만 observed object lifetime에 의해 실제 deinit시점에 뒤로 밀릴수도있고 이로인한 side effect가 발생할 수 있지않을까에대한 이야기가 아닐까 싶습니다

코드를 보면 traveler2가 마지막으로 사용되는시점(print("Done traveling")위)에 deinit이 불려야하지만 실제로는 observed object lifetime때문에 print("Done traveling")이후에 불릴수도있습니다. ARC의 최적화정도에 따라 다르지만 결국은 이것도 우연입니다...
우리가 개발할때는 Traveler의 print가 출력되고 test의 print가 출력된다고 생각하고 개발을 해야 side effect로 인한 문제에 직면하지 않을수 있습니다
그리고 deinitalizer로 인해 발생할수있는 문제도 weak로인한 side effect를 방지하는 방법을 통해서 예방해야합니다

xcode설정을 통해 observed object lifetime를 최대한 보장된 최소수명으로 맞춰주고 이를 통해 ARC의 최적화가 가능해집니다
즉, 우리는 ARC의 최적화를 통해 빠르게 컴파일하고 동작하는 코드를 사용하는것이 좋은데 이를 고려하려면 위에서 계속 설명했던것처럼 기존에 우연에 기대는 코드와 로직이 아닌 보장된 최소수명을 고려한 코드를 보고 읽고 작성할줄알아야 ARC optimization기능을 통해 최적화된 ARC를 사용할수 있게됩니다
정말정말 긴 요약본이 완성되었네요...
결국 보장된 최소수명으로 ARC최적화하는 기능을 새로 추가했으니까 보장된 최소수명을 고려한 개발을 할줄알아야해 그러니까 알려줄게 였네요 ㅎㅎ
제가 이 영상을 정리해야겠다고 마음먹은 가장 큰 이유는 구글에 검색을했을때 단어하나하나 해석을가지고 설명하는 글들이 많았어서 이해하기가 어려웠었습니다(사과가 뭔지 모르는데 자 이게 apple입니다 하는 설명을 듣는느낌이었달까요...) 그래서 최대한 영상에서 나오는 단어가 무슨의미이고 어떤 의도가 담겨있는 워딩인지를 최대한 자세하게 설명하려고 노력을해봤습니다ㅎㅎ
정말 긴글이지만 읽어보신다면 ARC에대한 새로운 관점과 측면에대해 알수있지않을까라는 생각이드네요
저는 다음번에도 멋진 글로 찾아뵙겠습니다!
그럼20000!