본문 바로가기
Dev/Swift 내 정리

[Swift 기본기] weak와 side table

by Mintta 2024. 3. 15.
내용이 수정될 수 있는 게시글입니다.

 

안녕하세요 정말 정말 오랜만에 돌아왔습니다.

 

오늘 오랜만에 들고 돌아온 주제는 side table입니다. 제가 전혀 몰랐다가 알게된거라 새로 알게된 겸 개념을 정리해보고자 합니다!

 

 

우선 제일 처음으로 제가 잘못? 알고 있었던 것 부터 바로잡고 가겠습니다.

 

swift arc

RefCount의 주석을 참고해보면 알 수 있듯이 Reference Count의 종류는 strong, unowned, weak 총 3가지로 각 각 존재합니다! 저는 reference count는 하나만 존재해서 이를 strong이면 +1 시켜주고, unowned나 weak면 그대로 두고 뭔가 이런식으로 하나를 가지고 조작하는 것이라고 생각했는데 그게 아니였습니다.

 

Strong RC

- 0이 되면 object가 deinited됩니다. (🤔 그런데 deinited이 메모리에서 할당 해제 되었다! 를 뜻하는 것은 아닙니다. 아직 그 전 단계입니다. 글 뒤에서 더 살펴봅시다)

- 0이 되면 unowned 읽기는 error를 내뱉고, weak 읽기는 nil이 됩니다.

 

Unowned RC

- strong reference의 extra +1의 unowned RC를 가지고 있다가, deinit이 완료가 되면 감소시킵니다. 

- unowned R가 0이 되면, object가 메모리에서 할당 해제(freed)됩니다.

 

Weak RC

- unowned reference의 extra +1의 weak RC를 가지고 있다가, 메모리 할당 해제(freed)가 완료가 되면 이를 감소시킵니다.

- 0이 되면, object의 side table entry(🤔)를 메모리 할당 해제 시킵니다(free).

 

이런 Reference Count들이 object에서 어떻게 관리되고 있는지를 더 살펴보겠습니다.

 

 

HeapObject를 살펴보면 안에 InlineRefCounts가 있고 안에 위에서 언급한 strong RC와 unowned RC가 보이네요. 그런데 weak RC는 어디에도 안보입니다. 하지만 HeapObjectSideTableEntry* 포인터가 가르키는 곳에 가보면 strong, unowned와 weak RC가 보이네요. 그리고 HeapObjectSideTableEntry* or Strong RC + Unowned RC로 되어있는 것으로 보아 line으로 strong과 unowned RC를 관리하던지, 혹은 SideTableEntry를 통해서 모든 RC들을 다른 곳에서 관리하는지 둘 중의 하나의 방법으로 동작하는 것을 의미하고 있습니다.

 

 

 

Object는 기본적으로 init될 때 side table없이 생성되고, weak reference가 생기면 그제서야 side table을 얻는다고 합니다.

아하 그럼 weak reference가 없는 객체는 strong, unowned RC를 inline에서 관리하고 weak reference가 생기면 해당 객체의 RC를 별도의 side table에서 관리한다는 뜻이구나 !

 

side table을 얻는 작업은 one-way operation으로 이것이 thread race를 방지한다고 합니다.

 

one-way operation이란 말은 객체가 일단 side table을 얻게 되면, 그 객체는 side table을 절대 잃지 않는다는 것을 말합니다.

 

그럼 side table은 무엇이고, side table을 one way operation으로 하는게 어떻게 thread race를 방지하는 걸까 ???? 🤔

 

Side table

side table 부터 한번 알아봅시다. side table을 알아보기 위해서 우선 side table이라는 게 왜 생겼는지부터 알아봅시다.

https://maximeremenko.com/swift-arc-weak-references 예제를 활용해서 설명하도록 하겠습니다.

 

Side table이 없던 Swift에서는...

class User {
	let id: Int
    let email: String
}

 

 

Side table이 없던 Swift에서는 이런식으로 모든 프로피터들과 RC들을 inline storage에 저장했습니다. 

 

만약 strong RC가 0으로 바뀌면서 weak RC가 1만 남아있는 상황이 된다고 가정해봅시다.

그렇다면, 해당 객체는 deinit 메서드가 불리면서 deinited 상태가 됩니다.

 

메모리에서 할당 해제 된다! 가 아닌 deinited 상태가 된다 입니다. 상태가 된다라고 표현한데에는 이유가 있습니다. 실제로 메모리에서 해당 객체가 할당 해제 된 것(deallocation)이 아니기 때문입니다.

 

그럼 언제 할당 해제가 되냐? 다시 weak 객체에 접근했을 때! 그제서야 비로소 weak RC가 1 감소되면서 메모리에서 할당 해제가 이루어지게 됩니다. 

 

그렇다면 'deinited 상태가 된 시점' 과 'deinited 이후 다시 해당 객체를 접근하는 시점' 사이의 공백 시간동안 해당 객체는 zombie object로서 메모리에서 남아있게 됩니다. 이건 그닥 좋아보이지 않습니다. 

 

따라서 이 문제를 해결하고자 등장한 것이 바로 side table입니다 !

 

Side table과 함께라면

"Object는 기본적으로 init될 때 side table없이 생성되고, weak reference가 생기면 그제서야 side table을 얻는다고 합니다."

 

weak reference가 존재하면 side table이 생성됩니다.

⚠️ 주의해야할 점은, side table이 없어도 weak RC가 0이상일 수 있습니다. 위에서 언급했듯이 weak RC는 unowned rc 추가로 +1만큼을 가지고 있기 때문에 "weak가 있으면 side table이 존재한다"는 항상 참이지만, "side table이 없으면 weak RC는 0이다"는 틀린 말이 됩니다.

unowned가 strong rc + 1, weak가 unowned rc + 1 이런식으로 +1을 더해주는 이유는 strong -> unowned -> weak 순으로 체크하기 위해 순서를 지키기 위함입니다.

따라서 weak RC를 포함한 모든 RC들을 side table에서 관리하는 형태가 될 것 입니다.

 

여기서 한가지 중요한 점은 바로 이 부분입니다.

 

Strong and unowned variables point at the object.
Weak variables point at the object's side table.

 

strong과 unowned 변수들은 객체를 가르킨데 반해, weak는 객체의 side table을 가르킨다.

 

그림으로 표현하면 아래 그림과 같습니다.

 

이제 해당 객체는 (with sidetable)은 weak reference와는 상관없이 deinit과 deallocation까지 모두 이루어질 수 없습니다. 더 이상 weak 참조의 눈치를 볼 필요가 없습니다!

 

이런 side table이 thread race를 방지할 수 있는 이유는 바로 한 곳에서 모든 rc들을 관리하기 때문입니다.

결국 모든 스레드가 동일한 위치에서 rc에 대해 Read/Write 작업을 하게 되고 이곳에 대한 atomicity (원자성, 불변성)만 지켜준다면 thread race를 막을 수 있을 것 입니다.

one-way 자체만으로 thread race를 예방했다 라기보다는 thread race문제를 해결하기 조금 더 원할하게 해준다 느낌 정도가 더 맞을 것 같습니다..!

 

이번 글은 여기서 마치겠습니다! 아직 object lifetime에 대한 이야기가 남았는데, 이곳에서 이어가기보다는 글을 조금 쪼개서 다음 글로 돌아오겠습니다!

 

 

추가 작은 실험

var a: EmptyClass? = EmptyClass()
weak var b = a

 

이렇게 해두면 변수 b는 a의 side table을 참조하게 될 것이기에, break point를 찍어서 메모리 주소를 확인해보면 다른 메모리주소가 찍히는건가? 싶어서 실험해본 결과..

 

 

동일한 메모리 주소를 가르키고 있었습니다. GPT답변과 제 생각을 더해서 결과를 해석보자면, 결국 side table은 개발자가 관여할 수 있는 부분이 아니고 정말 세부적인 구현 디테일이기 때문에 보여질 필요가 없고 오히려 개발자들을 더욱 혼란스럽게 만들지 않을까하는 생각입니다.

댓글