참조 타입(Reference Type)의 캡처(Capture)와 캡처 리스트(Capture List)

썹스·2022년 11월 18일
0

Swift 문법

목록 보기
42/68

참조 타입(Reference Type)의 캡처(Capture)와 캡처 리스트(Capture List)

참조 타입(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)이기 때문입니다.


강한 참조 사이클(Strong Reference Cycle)의 문제 해결

클래스로부터 만들어진 인스턴스(객체) 또는 클로저가 서로를 참조하여 발생하는 문제입니다.

✅ 강한 참조 사이클에 의한 메모리 누수 (참조 타입)

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)가 동작하고 있지 않음


📌 메모리 누수(Memory Leak) 해결 방법

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)가 동작

profile
응애 나 코린이(비트코인X 코딩O)

0개의 댓글