참조 타입(Reference Type)의 캡처와 캡처 리스트는 값 타입(Value Type)과 비슷한 형태를 가지고 있지만, 내부 동작 및 결과에 차이점이 있습니다.
⚙️ 참조 타입(Reference Type)의 캡처(Capture)
클래스로부터 만들어진 인스턴스의 속성값 주소를 클로저가 참조할 때 속성값의 주소가 할당된 변수의 주소를 캡처(Capture) 하여 클로저의 힙(Heap) 영역에 저장합니다.
⚙️ 참조 타입(Reference Type)의 캡처 리스트(Capture List)
클래스로부터 만들어진 인스턴스의 속성값 주소를 클로저(캡처 리스트)가 참조할 때 속성값의 주소 자체를 캡처(Capture)하여 클로저의 힙(Heap) 영역에 저장합니다.
(참조 타입에서 속성값의 주소 자체는 스택(Stack) 영역에 있습니다.)
✅ 참조 타입(Reference Type)의 캡처(Capture)와 캡처 리스트(Capture List) 코드 구현
class Number{ var num: Int init(num: Int){ self.num = num } } var A = Number(num: 10) //인스턴스 A 생성과 동시에 RC(참조 카운팅) 1 증가 var B = Number(num: 10) //인스턴스 B 생성과 동시에 RC(참조 카운팅) 1 증가 print("A의 값: \(A.num), B의 값: \(B.num)") // A의 값: 10, B의 값: 10 var captureList = { [A] in // 캡처 리스트를 위한 클로저 정의 + A 인스턴스의 RC 1 증가 print("A(캡처 리스트)의 값: \(A.num), B의 값: \(B.num)") } A.num = 100 // 초깃값 변경 B.num = 100 // 초깃값 변경 captureList() // A(캡처 리스트)의 값: 100, B의 값: 100 A.num = 777 // 캡처 리스트 클로저 동작 후 초깃값 변경 B.num = 777 // 캡처 리스트 클로저 동작 후 초깃값 변경 captureList() // A(캡처 리스트)의 값: 777, B의 값: 777 => "A: 100, B: 777"이 아님
✋캡처 리스트를 통해 클로저의 힙 영역에 저장된 속성값이 변할 수 있었던 이유는 근본적으로 참조 타입은 스택(stack) 영역의 값이 순수한 값이 아닌 힙을 참조하는 주솟값(address value)이기 때문입니다.
클래스로부터 만들어진 인스턴스(객체) 또는 클로저가 서로를 참조하여 발생하는 문제입니다.
✅ 강한 참조 사이클에 의한 메모리 누수 (참조 타입)
class Man{ var name: String var run: (()->Void)? init(name: String){ self.name = name } func runClosure(){ run = { print("\(self.name)이 달리고 있습니다.") } } deinit{ print("\(self.name) 메모리에서 제거되었습니다.") } } func doSomething(){ var kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가) kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성 } doSomething() // 아무 출력 없음
1️⃣
doSomething()
함수 작동
2️⃣ kim 인스턴스 생성 (kim RC 1 증가)
3️⃣ kim 인스턴스가runClosure()
함수 작동
4️⃣runClosure()
함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성 (클로저(run) RC 1 증가)
5️⃣runClosure()
함수에서 클로저(run)가 kim을 지목하여 참조하고 있음 (kim RC 1 증가)
6️⃣doSomething()
함수의 실행이 종료 (kim RC 1 감소)
➡️ 최종적으로 kim 인스턴스의 카운트는 1, 클로저(run)의 카운트는 1
➡️ kim 인스턴스와 클로저(run)가 강한 참조 사이클을 유지하고 있기 때문에 소멸자(deinit)가 동작하고 있지 않음
1️⃣ 캡처 리스트(Capture List) + 약한 참조(Weak Reference)를 활용하여 코드를 작성한다.
2️⃣ 캡처 리스트(Capture List) + 비소유/무소유 참조(Unowned Reference)를 활용하여 코드를 작성한다.
✅ 캡처 리스트(Capture List) + 약한 참조(Weak Reference)
- 약한 참조는 서로를 가리키는 인스턴스(객체)의 카운트 결과를 세지 않는 방식입니다.
- 약한 참조는 소유자(상위 인스턴스)보다 짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용합니다.
- 참조하고 있던 인스턴스가 메모리에서 제거되면, 참조했던 다른 한쪽의 인스턴스는 nil로 초기화됩니다.
- 약한 참조는 변수(var)로만 정의할 수 있고, 옵셔널 타입으로만 정의해야 합니다.
class Man{ var name: String var run: (()->Void)? init(name: String){ self.name = name } func runClosure(){ run = { [weak self] in print("\(self?.name)이 달리고 있습니다.") } } deinit{ print("\(self.name) 메모리에서 제거되었습니다.") } } func doSomething(){ var kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가) kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성 } doSomething() // 김철수 메모리에서 제거되었습니다.
1️⃣
doSomething()
함수 작동 ->
2️⃣ kim 인스턴스 생성 (kim RC 1 증가) ->
3️⃣ kim 인스턴스가runClosure()
함수 작동 ->
4️⃣runClosure()
함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성 (약한 참조에 의해 클로저(run) RC 0 증가) ->
5️⃣runClosure()
함수에서 클로저(run)가 kim을 지목하여 참조하고 있음 (kim RC 1 증가) ->
6️⃣doSomething()
함수의 실행이 종료, 함수가 종료됨에 따라 kim 인스턴스가 메모리에서 제거되기 때문에 클로저(run) 또한 메모리에서 제거된다. (kim RC 1 감소)
➡️ 최종적으로 kim 인스턴스의 카운트는 0, 클로저(run)의 카운트는 0
➡️ kim 인스턴스와 클로저(run)의 참조 카운트가 0이 되었기 때문에 소멸자(deinit)가 동작
✅ 캡처 리스트(Capture List) + 비소유/무소유 참조(Unowned Reference)
- 비소유/무소유 참조는 서로를 가리키는 인스턴스(객체)의 카운트 결과를 세지 않는 방식입니다.
- 비소유/무소유 참조는 소유자(상위 인스턴스)보다 길거나 같은 생명주기를 가진 인스턴스를 참조할 때 주로 사용합니다.
- 참조하고 있던 인스턴스가 메모리에서 제거되면, 참조했던 다른 한쪽의 인스턴스는 nil로 초기화되지 않습니다.
- 비소유/무소유 참조는 변수(var), 상수(let) 둘 다 정의할 수 있고, 다양한 타입으로 정의할 수 있습니다.
class Man{ var name: String var run: (()->Void)? init(name: String){ self.name = name } func runClosure(){ run = { [unowned self] in print("\(self.name)이 달리고 있습니다.") } } deinit{ print("\(self.name) 메모리에서 제거되었습니다.") } } func doSomething(){ var kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가) kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성 } doSomething() // 김철수 메모리에서 제거되었습니다.
1️⃣
doSomething()
함수 작동 ->
2️⃣ kim 인스턴스 생성 (kim RC 1 증가) ->
3️⃣ kim 인스턴스가runClosure()
함수 작동 ->
4️⃣runClosure()
함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성 (약한 참조에 의해 클로저(run) RC 0 증가) ->
5️⃣runClosure()
함수에서 클로저(run)가 kim을 지목하여 참조하고 있음 (kim RC 1 증가) ->
6️⃣doSomething()
함수의 실행이 종료, 함수가 종료됨에 따라 kim 인스턴스가 메모리에서 제거되기 때문에 클로저(run) 또한 메모리에서 제거된다. (kim RC 1 감소)
➡️ 최종적으로 kim 인스턴스의 카운트는 0, 클로저(run)의 카운트는 0
➡️ kim 인스턴스와 클로저(run)의 참조 카운트가 0이 되었기 때문에 소멸자(deinit)가 동작