티스토리 뷰

1. Memory Basics

메모리를 가장 단순하게 이야기하면 0과 1을 저장할 수 있는 반도체입니다. 메모리는 전압 차이를 이용해서 데이터를 저장합니다. 전기가 들어오면 1이 저장되고 전기가 들어오지 않으면 0이 저장됩니다.

 

0이나 1이 들어가는 가장 작은 공간을 Bit 라고 합니다. 여기에 들어가는 모든 데이터는 종류에 상관 없이 2진수로 변환되어 저장됩니다. Bit 는 컴퓨터 공학에서 정보의 기본 단위로 사용되고 있습니다. 프로그래밍에서는 8개의 Bit 가 모인 Byte 를 기본 단위로 사용합니다.

 

1byte 에는 256개의 Binary 패턴이 있습니다. 양수만 저장한다고 생각하면 저장할 수 있는 값의 범위는 0부터 255 사이입니다. 양수와 음수를 모두 저장하는 경우에는 -128에서 127까지의 수를 저장할 수 있습니다.

 

나열된 Bit 중 가장 왼쪽에 있는 비트를 최상위 비트(Most Significant Bit)라고 하고 가장 오른쪽에 있는 비트를 최하위 비트(Least Significant Bit)라고 합니다. 실제 데이터를 저장하는 비트는 Data Bit 라고 합니다. 양수만 저장할 때는 모든 비트를 Data Bit 로 사용하고 양수와 음수를 저장하는 경우에는 최상위 비트를 부호를 구분하는 비트로 사용하고 나머지 7개의 비트만을 Data Bit 로 사용합니다. 최상위 비트의 값이 0이면 양수(Positive Numbers)로 인식하고 1이면 음수(Negative Numbers)로 인식합니다. 이런 역할을 하는 비트를 부호 비트(sign Bit)라고 합니다.

 

초기의 컴퓨터는 음수를 저장하기 위해서 단순히 부호 비트를 1로 바꾸고 나머지 비트는 양수와 동일한 비트를 사용하였습니다. 하지만 이 방식은 여러가지 문제로 인해 더이상 사용하지 않습니다. 현재는 2의 보수 방식을 사용하고 있습니다. 2의 보수 방식은 원래의 비트 값에 Bitwise Not 연산을 수행한 뒤 1을 더하는 것입니다. Bitwise Not 연산은 Bit 가 1인 경우 0으로 바꾸고 0인 경우 1로 바꾸는 비트 연산입니다.

 

메모리에는 1byte 를 저장할 수 있는 공간마다 고유한 주소가 있습니다. CPU 는 이 주소를 사용하여 메모리에 접근합니다. CPU 는 메모리 주소를 저장하고 특정 위치에 접근하기 위해서 Memory Address Register 를 사용합니다. CPU 는 보통 32bit 와 64bit 로 구분합니다. 주소 레지스터의 크기는 CPU 의 비트 수와 같습니다. 32bit CPU 는 주소 레지스터를 통해 약 42억개의 메모리주소에 접근할 수 있습니다. 이것을 기가 바이트 단위로 환산하면 4GB 입니다. 32bit CPU 를 사용하는 컴퓨터에서 최대 메모리 용량이 4GB 로 제한되는 것은 바로 이런 이유 때문입니다. 64bit CPU 의 경우 약 1800경개가 넘는 메모리 주소에 접근할 수 있습니다. 사용 가능한 메모리 공간도 이론적으로는 16EB 로 제한됩니다. 하지만 실제로는 메모리 운영 방식에 따라서 더 작은 크기로 제한됩니다.

 

운영체제는 프로그램을 실행할 떄마다 프로그램이 사용할 메모리 공간을 할당합니다. 이 공간은 사용 용도에 따라서 크게 4가지로 구분됩니다.

 

Code 영역에는 기계어로 번역된 프로그램 코드가 저장됩니다.

 

Data 영역에는 정적 변수와 전역 변수가 저장됩니다. 여기에 저장된 데이터는 프로그램이 시작될 때 생성되었다가 프로그램이 제거되면 함께 제거됩니다.

 

Stack 에는 지역변수, 파라미터, 리턴값이 저장됩니다. Stack 은 함수 호출과 밀접한 관련이 있습니다. 함수를 호출하면 함수에서 사용하는 모든 값을 저장하는 메모리 공간이 자동으로 생성됩니다. 이 공간을 stack frame 이라고 부릅니다. stack frame 은 함수의 실행이 종료되면 Stack 에서 자동으로 제거되고 다른 함수에서 메모리를 다시 사용할 수 있게 됩니다. Stack 은 stack frame 을 쌓아가는 방식으로 메모리를 관리합니다. 함수가 실행될 때마다 새로운 stack frame 이 생성됩니다. 반대로 함수의 실행이 종료되면 가장 최근에 추가된 stack frame 부터 순서대로 제거됩니다.

 

Heap 에는 동적으로 할당된 데이터가 저장됩니다. 다른 영역은 할당할 공간의 크기를 예측할 수 있지만 Heap 은 동적으로 할당되는 특성때문에 공간의 크기를 예측하기 어렵습니다. Heap 에 저장된 데이터는 생성 시점과 제거 시점이 정해져있지 않습니다. 그래서 자동으로 생성되거나 제거되지 않고 직접 코드를 통해 생성하고 제거해야 합니다. 더 이상 필요하지 않은 데이터를 제거하지 않고 그대로 둔다면 프로그램이 종료될 때까지 유지되고 이런 메모리가 많아진다면 결국 메모리가 부족해집니다. 더 이상 필요하지 않은 메모리가 정상적으로 해제되지 않는 것을 메모리 누수(Memory Leaks)라고 합니다.

 

값 형식(Value Type)은 Stack 에 저장됩니다. 더 이상 사용하지 않는 경우 자동으로 제거되기 때문에 메모리 관리가 상대적으로 쉽습니다. 반면 참조 형식(Reference Type)의 경우 실제 값을 Heap 에 저장하고 Heap 메모리 주소를 Stack 에 저장합니다. 실제 값에 접근할 때는 항상 Stack 에 있는 주소를 통해 접근합니다. 값을 더 이상 사용하지 않는다면 두 공간에 저장되어 있는 데이터를 완전히 삭제해서 메모리 누수를 방지하는 것이 중요합니다.

 

2. Value Type vs Reference Type

Swift 에서 Stucture, Enumeration, Tuple 은 값 형식으로 분류하고, Class, Closure 는 참조 형식으로 분류합니다.

 

먼저 값 형식에 대해 알아보겠습니다.

struct SizeValue {
    var width = 0.0
    var height = 0.0
}

var value = SizeValue()

 

이렇게 인스턴스를 생성하면 Stack 에 메모리 공간이 생성되고 여기에는 0.0으로 초기화된 값이 저장됩니다. 그리고 value 변수와 메모리 공간이 연결됩니다.

var value2 = value

 

이 경우 값 형식에서는 값이 복사됩니다. value 인스턴스의 복사본이 새로운 메모리에 저장되고 이 메모리와 value2 변수가 연결됩니다. 두 인스턴스의 속성값은 모두 동일하지만 서로 다른 메모리에 저장된 개별 인스턴스입니다.

value2.width = 1.0
value2.height = 2.0

 

이렇게 하면 value2 와 연결된 메모리가 새로운 값으로 업데이트됩니다. 하지만 value 변수와 연결된 메모리는 아무런 변화가 없습니다.

value   // width: 0.0, height: 0.0
value2  // width: 1.0, height: 2.0

 

값 형식은 항상 stack 에 저장됩니다. 그리고 값을 전달할 때마다 새로운 복사본이 생성됩니다.

 

다음으로 참조 형식을 살펴보겠습니다.

class SizeObject {
    var width = 0.0
    var height = 0.0
}

var object = SizeObject()

 

인스턴스를 생성하면 Stack 과 Heap 에 새로운 메모리 공간이 생성됩니다. Heap 에는 인스턴스가 저장되고 Stack 에는 Heap 메모리 주소가 저장됩니다. 그리고 object 변수는 Stack 에 저장된 메모리와 연결됩니다. 값 형식과 달리 인스턴스에 바로 접근할 수 없고 항상 Stack 을 거쳐서 접근합니다.

var object2 = object

 

이렇게 하면 Stack 에 새로운 메모리 공간이 생성되고 여기에는 바로 전에 Stack 에 저장했던 주소가 저장됩니다. Heap 에서는 새로운 메모리 공간이 생성되거나 인스턴스가 복사되지는 않습니다. 어떤 변수를 통해서 접근하더라도 결과적으로 Heap 에 있는 동일한 인스턴스에 접근하게 됩니다.

object2.width = 1.0
object2.height = 2.0

 

object2 변수를 통해 변경하고 있지만 object 변수를 통해 변경한 것과 동일합니다. 두 변수가 최종적으로 동일한 인스턴스에 접근하기 때문입니다.

object   // width: 1.0, height: 2.0
object2  // width: 1.0, height: 2.0

 

참조 형식은 heap 에 인스턴스를 저장하고 Stack 에 메모리 주소를 저장합니다. 값을 전달할 때마다 인스턴스 복사본이 생성되지는 않습니다. 대신 stack 에 저장되어 있는 주소가 복사됩니다.

 

"참조를 전달한다" 는 표현과 "참조를 복사한다" 는 표현에서 참조는 Stack 에 저장되어 있는 메모리 주소를 의미합니다. 동일한 인스턴스에 접근하는 메모리 주소가 복사되기 때문에 주소를 여러 번 복사해도 접근 대상이 달라지는 것은 아닙니다. 그래서 어떤 변수를 통해 속성을 바꾸더라도 동일한 인스턴스의 속성을 바꾸게 됩니다.

 

이번에는 값 형식과 참조 형식을 상수에 저장해 보겠습니다.

let v = SizeValue()
// v.width = 1.0 - 에러
// v.height = 2.0 - 에러

let o = SizeObject()
o.width = 1.0
o.height = 2.0

 

값 형식의 인스턴스를 상수에 저장하면 모든 속성이 상수가 됩니다. 그래서 속성을 변경하는 코드를 작성하면 속성을 바꿀 수 없다는 에러가 발생합니다. let 키워드는 상수가 가리키는 Stack 을 값을 바꾸지 못하는 공간으로 만들어버립니다. 그래서 속성이 변수로 선언되어 있지만 메모리에 저장된 값을 바꿀 수 없기 때문에 속성에 저장된 값을 바꾸는 것은 불가능합니다.

 

참조 형식의 경우를 보면 이번에도 let 키워드는 상수가 가리키는 Stack 을 값을 바꿀 수 없도록 제한합니다. 참조 형식에서 Stack 에는 메모리 주소가 저장되어 있습니다. 여기에 저장되어 있는 메모리 주소를 바꿀 수 없기 때문에 상수가 가리키는 인스턴스를 바꿀 수 없습니다. 반면 인스턴스가 저장되어 있는 Heap 은 아무런 제한이 없습니다. 그래서 값 형식과 달리 인스턴스 속성을 마음대로 바꿀 수 있습니다.

 

마지막으로 비교 연산자와 항등 연산자를 메모리 관점에서 살펴보겠습니다. 비교 연산자(== or !=)는 값 형식을 비교할 때 Stack 에 저장된 값을 비교합니다. 반면 참조 형식을 비교할 때는 Heap 에 저장된 값을 비교합니다. 다시 말해서 형식에 관계 없이 항상 실제 값을 비교합니다. 참조 형식에서 Stack 에 저장된 주소를 비교할 때는 항등 연산자(=== or !==)를 사용합니다.

 

3. ARC(Automatic Reference Counting)

Stack 에 저장된 데이터는 자동으로 제거되기 때문에 특별한 관리가 필요 없습니다. 하지만 Heap 저장되는 데이터는 필요하지 않은 시점에 직접 제거해야 합니다. 메모리 저장 모델은 Heap 에 저장되는 데이터를 관리합니다. 다시 말하면 클래스 인스턴스의 메모리를 관리합니다.

 

Swift 는 Objective-C 와 동일한 메모리 관리 모델을 사용합니다. Apple 이 제공하는 개발 환경을 Cocoa 라고 하는데 여기에서 사용하는 메모리 관리 모델입니다. Cocoa 에서 사용하는 메모리 관리 모델은 2가지입니다. Objective-C 에서는 두 모델을 모두 사용할 수 있지만 Swift 는 ARC 모델만 지원합니다. 두 모델을 이해하기 위해서는 소유 정책(Ownership Policy)과 참조 카운트(Reference Count)를 이해해야 합니다.

 

인스턴스는 하나 이상의 소유자가 있는 경우 메모리에 유지됩니다. 반대로 소유자가 없다면 메모리에서 제거됩니다. 제거 시점을 파악하기 위해서 소유자 수를 저장하는데 이것을 참조 카운트라고 합니다. 참조 카운트로 다시 설명하면, 인스턴스는 참조 카운트가 1이상이면 메모리에 유지되고 참조 카운트가 0이 되면 메모리에서 제거됩니다.

 

클래스 인스턴스를 변수에 저장하면 변수가 소유자가 됩니다. 이 시점에 인스턴스의 참조 카운트는 1입니다. 또 다른 변수가 인스턴스를 소유하면 참조 카운트는 2가 됩니다. 인스턴스를 소유하기 위해서는 특별한 메시지를 전달해야 합니다. 코드 레벨에서 보자면 인스턴스가 제공하는 retain 메소드를 호출하는 것과 같습니다. 인스턴스가 더 이상 필요하지 않다면 소유권을 포기해야 합니다. 소유권 획득과 마찬가지로 특별한 메시지를 전달합니다. 이 메시지는 release 메시지입니다.

 

MRC(Manual Reference Counting) 에서는 소유 정책과 관련된 코드를 하나부터 열까지 직접 구현합니다. 하지만 소유권을 제대로 처리하는 것은 정말 어렵고 많은 경험을 요구합니다. 메모리 오류가 발생할 가능성도 상당히 높습니다. 그래서 프로그래밍의 안정성이 낮아지고 디버깅이 어려워집니다.

 

이런 문제를 해결하기 위해 ARC(Automatic Reference Counting) 이 도입되었습니다. ARC 는 소유 정책을 자동으로 처리합니다. 소유 정책과 참조 카운트를 처리하는 방식은 MRC 와 동일합니다. 하지만 컴파일러가 메모리 관리 코드를 자동으로 추가해주기 때문에 관련된 코드를 직접 작성할 필요가 없습니다. 작성해야 하는 코드의 양이 줄어들고 프로그램의 안정성도 증가합니다.

 

Swift 는 ARC 를 기본 모델로 사용합니다. MRC 는 여러가지 단점으로 인해 Swift 에서는 사용하지 않습니다. Objective-C 는 두 모델을 모두 지원하지만 특별한 이유가 없다면 ARC 를 사용합니다.

 

ARC 는 3가지 참조를 제공합니다.

 

1. Strong Reference (강한 참조): 기본적으로 인스턴스와 소유자는 강한 참조로 연결됩니다. 대상을 소유할 때마다 참조 카운트가 1씩 증가하고 소유권을 포기할 때마다 1씩 감소합니다. 그리고 인스턴스는 소유자가 존재하는 동안에는 메모리에서 제거되지 않습니다.

 

2. Weak Reference (약한 참조)

3. Unowned Reference (비소유 참조)

 

먼저 기본적인 ARC 가 이루어 지는 과정을 살펴보겠습니다.

class Person {
    var name = "John Doe"
    
    deinit {
        print("person deinit")
    }
}

var person1: Person?
var person2: Person?
var person3: Person?

person1 = Person()  // 참조 카운트 1
person2 = person1   // 참조 카운트 2
person3 = person1   // 참조 카운트 3

person1 = nil  // 참조 카운트 2
person2 = nil  // 참조 카운트 1
person3 = nil  // 참조 카운트 0 -> 소멸자 호출 시점

 

4. String Reference Cycle

강한 참조 때문에 인스턴스를 제대로 해제할 수 없는 경우를 강한 참조 사이클이라고 합니다. ARC 는 메모리 관리를 대신 처리해 주지만 강한 참조 사이클까지 자동으로 처리하지는 못합니다.

 

강한 참조 사이클이 발생하는 경우를 살펴보겠습니다.

class Person {
    var name = "Jokn Doe"
    var car: Car?
    
    deinit {
        print("person deinit")
    }
}

class Car {
    var model: String
    var lessee: Person?
    
    init(model: String) {
        self.model = model
    }
    
    deinit {
        print("car deinit")
    }
}

var person: Person? = Person()               // Person 인스턴스의 참조 카운트 1 증가
var rentedCar: Car? = Car(model: "Porsche")  // Car 인스턴스의 참조 카운트 1 증가

// 속성과 인스턴스가 강한 참조로 연결되고 car 인스턴스의 참조 카운트는 2가 됩니다.
person?.car = rentedCar

// Person 인스턴스의 참조 카운트는 2가 됩니다.
rentedCar?.lessee = person

person = nil     // person 변수가 접근하는 참조는 사라졌지만 Car 인스턴스에서 참조하기 때문에 참조 카운트는 1입니다.
rentedCar = nil  // rentedCar 변수가 접근하는 참조는 사라졌지만 Person 인스턴스에서 참조하기 때문에 참조 카운트는 1입니다.

 

위의 경우 참조 카운트가 남아있어 여전히 메모리에서 제거 되지 않습니다. 두 인스턴스가 속성을 통해 서로를 소유하고 있기 때문입니다. 여기에서 문제는 person 변수와 rentedCar 변수에 nil 을 저장해서 더 이상 접근할 방법이 없다는 것입니다. 그래서 두 인스턴스를 정상적으로 해지할 방법이 없습니다.

 

강한 참조 사이클은 약한 참조(Weak Reference)와 비소유 참조(Unowned Reference)를 통해 해결합니다. 두 방식 모두 인스턴스 사이의 강한 참조를 제거하는 방식으로 문제를 해결합니다. 강한 참조와 달리 참조 카운트를 증가시키거나 감소시키지 않습니다. 그래서 참조를 통해 인스턴스에 접근할 수는 있지만 인스턴스가 사라지지 않도록 유지하는 것은 불가능합니다.

 

먼저 약한 참조를 살펴보겠습니다.

약한 참조는 인스턴스를 참조하지만 소유하지는 않습니다. 참조 카운트도 변하지 않습니다. 그래서 인스턴스를 참조하고 있는 동안 대상 인스턴스는 언제든지 사라질 수 있습니다. 이런 특징 때문에 소유자에 비해 짧은 생명 주기를 가지는 인스턴스를 참조할 때 주로 사용합니다. 약한 참조는 항상 Optional 형식으로 선언합니다. 그리고 var 키워드 앞에 weak 키워드를 추가합니다. 참조하고 있는 인스턴스가 해제되면 자동으로 nil 로 초기화 됩니다.

class Person {
    var name = "Jokn Doe"
    var car: Car?
    
    deinit {
        print("person deinit")
    }
}

class Car {
    var model: String
    weak var lessee: Person?
    
    init(model: String) {
        self.model = model
    }
    
    deinit {
        print("car deinit")
    }
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar
rentedCar?.lessee = person

person = nil
rentedCar = nil

 

결과적으로 Person 인스턴스와 Car 인스턴스 모두 소멸자에 작성한 코드가 실행되며 정상적으로 메모리에서 해제됩니다.

 

다음으로 비소유 참조를 살펴보겠습니다.

비소유 참조는 약한 참조와 동일한 방식으로 강한 참조 사이클을 해결합니다. 하지만 Optional 형식이 아니라 Non-Optional 형식입니다. 참조 사이클을 해결하면서 속성을 Non-Optional 로 선언해야 할 때 주로 사용합니다. 그리고 인스턴스의 생명 주기가 소유자와 같거나 더 긴 경우에 주로 사용합니다. 비소유 참조는 unowned 키워드를 통해 생성됩니다. 그리고 약한 참조와 달리 Non-Optional 형식으로 선언합니다. Optional 형식이 아니기 때문에 참조하고 있는 인스턴스가 해제되어도 nil 로 초기화되지 않습니다. 그래서 해제된 인스턴스에 접근할 경우 런타임 에러가 발생합니다.

class Person {
    var name = "Jokn Doe"
    var car: Car?
    
    deinit {
        print("person deinit")
    }
}

class Car {
    var model: String
    unowned var lessee: Person
    
    init(model: String, lessee: Person) {
        self.model = model
        self.lessee = lessee
    }
    
    deinit {
        print("car deinit")
    }
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche", lessee: person!)

person?.car = rentedCar

person = nil
rentedCar = nil

 

5. Closure Capture List

클로저에서도 강한 참조 사이클이 발생합니다. 클로저가 인스턴스를 캡처하고 인스턴스가 클로저를 강한 참조로 저장하고 있다면 인스턴스가 정상적으로 해제되지 않습니다. 이 경우에도 약한 참조와 비소유 참조를 통해 문제를 해결합니다.

 

먼저 클로저에서 강한 참조 사이클이 발생할 수 있는 코드를 살펴보겠습니다.

class Car {
    var totalDrivingDistance = 0.0
    var totalUsedGas = 0.0
    
    lazy var gasMileage: () -> Double = {
        return self.totalDrivingDistance / self.totalUsedGas
    }
    
    func drive() {
        self.totalDrivingDistance = 1200.0
        self.totalUsedGas = 73.0
    }
    
    deinit {
        print("car deinit")
    }
}

 

gasMileage 속성은 지연 속성으로 저장되어 있습니다. 그리고 여기에는 Double 을 리턴하는 클로저가 저장되어 있습니다. 그리고 클로저는 self expression 을 통해 속성에 접근하고 있습니다.

 

self 는 인스턴스 자체를 나타내는 특별힌 속성입니다. 이 속성을 클로저에서 사용하면 self 가 나타내는 인스턴스가 캡처됩니다. 클로저는 실행이 종료될 때까지 self 를 강한 참조로 캡처합니다. 그래서 self 가 나타내는 인스턴스는 클로저의 실행이 완료될 때까지 메모리에서 제거되지 않습니다.

 

그리고 인스턴스를 캡처하는 클로저는 위와 같이 인스턴스 속성에 저장되어 있습니다. 속성이 약한 참조나 비소유 참조로 선언되어 있지 않기 때문에 인스턴스는 속성에 저장된 클로저를 강하게 참조합니다. 결과적으로 클로저와 인스턴스는 강한 참조로 연결됩니다.

var myCar: Car? = Car()
myCar?.drive()

 

위 코드까지 실행한 시점에는 아직 클로저가 실행되지 않았고 따라서 강한 참조 사이클도 발생하지 않았습니다. 그래서 이 시점에 myCar 속성에 nil 을 저장하면 인스턴스가 정상적으로 해제됩니다. 그리고 콘솔을 보면 소멸자에 추가해둔 코드가 출력됩니다.

 

이번에는 gasMileage 에 저장되어 있는 클로저를 호출하겠습니다.

myCar?.gasMileage()

 

이 시점에 클로저가 실행되고 self 를 캡처합니다. 여기에서 self 는 myCar 인스턴스입니다. 그러면 클로저가 self 를 통해 인스턴스를 소유하게 되고 강한 참조 사이클이 발생합니다. 다시 myCar 속성에 nil 을 저장하고 실행해보면 이번에는 소멸자에 추가해둔 코드가 실행되지 않습니다. 강한 참조 때문에 인스턴스가 정상적으로 해제되지 않습니다.

 

이제 Closure Capture List 를 통해 문제를 해결해 보겠습니다. 먼저 문법을 살펴보겠습니다.

{ [list] (parameters) -> ReturnType in
    code
}

// 단축 문법 사용시 보통 in 을 생략하지만 클로저 body 와의 구분을 위해 Closure Capture List 에서는 생략하지 않습니다.
{ [list] in
    code
}

 

이제 list 부분에 집중해 보겠습니다. 클로저 캡처 리스트는 세 가지 형태로 작성합니다.

 

1. 값 형식(Value Type)을 캡처할 때는 대상의 이름만 작성합니다.

{ [valueName] in
    code
}
var a = 0
var b = 0
let c = { print(a, b) }

a = 1
b = 2
c()

 

클로저가 값을 캡처할 때는 복사본이 아닌 참조가 전달됩니다. 따라서 변수의 값을 변경한 후 클로저를 실행해보면 변경된 값이 출력됩니다.

 

이번에는 a 변수를 클로저 캡처 리스트에 추가해보겠습니다.

let c = { [a] in print(a, b) }

a = 1
b = 2
c()

 

그리고 다시 코드를 실행해 보면 a 의 값은 캡처 시점에 저장된 값으로 출력됩니다. 값 형식을 클로저 캡처 리스트에 추가하면 참조 대신 복사본을 캡처합니다.

 

2, 3. 참조 형식(Reference Type)을 보겠습니다. 참조 형식을 캡처할 때는 반드시 weak 키워드나 unowned 키워드를 추가해야 합니다. weak 는 약한 참조로 캡처하고 unowned 는 비소유 참조로 캡처합니다.

{ [weak instanceName, unowned instanceName] in
    statements
}
class Car {
    var totalDrivingDistance = 0.0
    var totalUsedGas = 0.0
    
    lazy var gasMileage: () -> Double = { [weak self] in
        // 약한 참조는 Optional 형식입니다.
        // 따라서 self 를 사용할 때 지금처럼 unWrapping 하여 사용하거나 Optional Chaining 으로 멤버에 접근하여야 합니다.
        guard let strongSelf = self else { return 0.0}
        
        return strongSelf.totalDrivingDistance / strongSelf.totalUsedGas
    }
    
    func drive() {
        self.totalDrivingDistance = 1200.0
        self.totalUsedGas = 73.0
    }
    
    deinit {
        print("car deinit")
    }
}

 

클로저의 실행이 완료되지 않은 시점에 캡처 대상이 해제될 수 있다면 주로 약한 참조를 사용합니다. 만약 Car 인스턴스가 해제된 시점에 클로저가 호출되었다면 self 는 nil 이 됩니다. 그래서 Optional Binding 이 실패하고 코드가 종료됩니다. 약한 참조로 캡처할 때는 캡처 대상이 해제되는 경우도 함께 고려해야 합니다. 클로저에서 인스턴스를 약한 참조로 캡처하고 있기 때문에 더 이상 강한 참조 사이클은 발생하지 않습니다. 따라서 소멸자의 작성한 코드가 실행되며 정상적으로 해제됩니다.

 

약한 참조를 비소유 참조로 변경하면 Optional Binding 이나 Optional Chaining 은 필요 없습니다.

class Car {
    var totalDrivingDistance = 0.0
    var totalUsedGas = 0.0
    
    lazy var gasMileage: () -> Double = { [unowned self] in
        return self.totalDrivingDistance / self.totalUsedGas
    }
    
    func drive() {
        self.totalDrivingDistance = 1200.0
        self.totalUsedGas = 73.0
    }
    
    deinit {
        print("car deinit")
    }
}

 

코드를 살펴보면 클로저 캡처 리스트를 사용하지 않은 코드와 동일한 방식으로 속성에 접근하고 있습니다. 하지만 여기에서 사용한 self 는 비소유 참조로 캡처되었기 때문에 더 이상 강한 참조 사이클 문제를 발생시키지 않습니다. 그래서 코드를 다시 실행하면 인스턴스가 정상적으로 해제됩니다. 비소유 참조로 캡처한 대상은 클로저 실행이 종료되기 전에 해제될 수 있습니다. 해제된 대상에 접근할 경우 런타임 에러가 발생합니다. 그래서 비소유 참조는 캡처 대상의 생명주기가 클로저와 같거나 더 긴 경우에 주로 사용합니다.

'swift > 문법' 카테고리의 다른 글

26. Swift 문법 - Error Handling  (0) 2020.10.27
25. Swift 문법 - Generics  (0) 2020.10.27
23. Swift 문법 - Protocol 2  (0) 2020.10.26
22. Swift 문법 - Protocol 1  (0) 2020.10.26
21. Swift 문법 - Extensions  (0) 2020.10.26
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함