Swift에서는 앱의 메모리 사용을 관리하기 위해 ARC를 사용한다. 자동으로 참조 획수를 관리하기 때문에 대부분의 경우에 개발자는 메모리 관리에 신경 쓸 필요가 없고 ARC가 알아서 더이상 사용하지 않는 인스턴스를 메모리에서 해지한다. 하지만 몇몇의 경우 ARC에서 메모리 관리를 위해 코드의 특정 부분에 대한 관계에 대한 정보를 필요로 한다. 참조 횟수는 클래스 타입의 인스턴스에만 적용되고 값 타입인 구조체 열거형 등에는 적용되지 않는다.
클래스의 새 인스턴스를 만들 때마다 ARC는 인스턴스 정보를 담는데 필요한 적정한 크기의 메모리를 할당한다. 이 메모리는 그 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값도 갖고 있다. 추가적으로 인스턴스가 더이상 사용되지 않을 때 ARC는 그 인스턴스가 차지하고 있는 메모리를 해지해서 다른 용도로 사용할 수 있도록 공간을 확보해둔다. 하지만 만약 ARC가 아직 사용중인 인스턴스를 메모리에서 내렸는데 인스턴슨의 프로퍼티에 접근한다면 앱은 크래시가 발생하게 된다. ARC에서는 아직 사용중인 인스턴스를 해지하지 않기 위해 얼마나 많은 프로퍼티, 상수 혹은 변수가 그 인스턴스에 대한 참조를 갖고 있는지 추적한다. 그래서 ARC는 최소 하나라도 그 인스턴스에 대한 참조가 있는 경우 그 인스턴스를 메모리에서 해지 하지 않는다.
ARC가 실제 어떻게 동작하는지 예제 코드를 통해 확인해보자.
아래 코드는 하나의 클래스를 선언하고 클래스의 인스턴스가 생성될 때와 해지될 때 print
를 통해 로그를 찍게 구현한 클래스이다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
위에서 선언한 Person
클래스 타입을 갖는 reference 변수 3개를 선언한다. 이 변수는 모두 옵셔널 변수이다. 그래서 초기값으로 모두 nil를 갖는다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
하나의 변수에 Person
인스턴스를 생성해 참조하도록 한다.
reference1 = Person(name: "John Appleseed")
나머지 두 변수를 첫 번째 변수를 참조하도록 한다.
reference2 = reference1
reference3 = reference1
이 경우 reference2
, reference3
모두 처음에 reference1
이 참조하고 있는 같은 Person
인스턴스를 참조하게 된다. 이 시점에 Person
인스턴스에 대한 참조 횟수는 3이 된다. 그리고 나서 reference1
, reference2
두 변수의 참조를 해지한다. 그렇게 되면 Person
인스턴스에 대한 참조 횟수는 아직 1이어서 Person
인스턴스는 해지되지는 않는다.
reference1 = nil
reference2 = nil
Person
인스턴스를 참조하고있는 나머지 변수 reference3
의 참조를 해지하면 더이상 Person
인스턴스를 참조하고있는 것이 없으므로 ARC가 Person
인스턴스를 메모리에서 해지하게 된다.
reference3 = nil
// Prints "John Appleseed is being deinitialized"
위에 해지 로그가 찍히는 것으로 Person
인스턴스가 메모리에서 내려 갔음을 확인할 수 있다.
앞선 예제에서 본 것처럼 ARC에서 기본적으로 참조 횟수에 대해 추적하고 있기 때문에 더이상 사용하지 않는 인스턴스는 자동으로 메모리에서 해제되게 된다. 하지만 절대로 메모리에서 해제 되지 않는 경우도 있는데 클래스의 인스턴스간 강하게 상호 참조를 하고 있는 경우가 바로 그 경우이다. 이 것을 강한 순환 참조라고 부른다.
예제를 통해 어떻게 강한 순환 참조가 발생하는지 알아보자.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
위 예제 코드를 보면 Person
이라는 클래스는 변수로 Apartment
클래스의 인스턴스를 소유하고 있고 그 Apartment
클래스에서는 변수로 Person
형의 인스턴스를 소유하고 있다. 이렇게 변수를 선언하고 인스턴스를 생성하면 어떻게 되는지 알아보자.
var john: Person?
var unit4A: Apartment?
선언한 변수에 각각에 맞는 타입의 인스턴스를 생성한다. 지금까지는 아무런 문제가 없다.
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
현재까지의 변수와 인스턴스 상태를 그림으로 본다면 다음과 같다. 변수 john
은 Person
인스턴스를 참조하고 있고 변수 unit4A
는 Apartment
인스턴스를 참조하고 있다.
이 상황에서 john
의 apartment
변수에 unit4A
를 할당하고 unit4A.tenant
에 john
을 할당한다.
john!.apartment = unit4A
unit4A!.tenant = john
인스턴스 안의 apartment
와 tenant
가 각각 Apartment
, Person
인스턴스를 참조하고 있는 상황이 된다. 즉, Person
인스턴스의 참조 횟수는 2, Apartment
의 인스턴스 참조 횟수도 마찬가지로 2가 된다.
이 시점에 각 변수에 nil을 할당해 참조를 해지한다. 원래 의도한 것은 각 변수가 참조하고 있던 Person
과 Apartment
인스턴스가 해지되는 것이다. 그러나 이 두 인스턴스는 해지 되지 않는다.
john = nil
unit4A = nil
각 변수에 nil을 할당한 시점에서의 참조 상황은 다음 그림과 같다. 변수 john
과 unit4A
는 각 인스턴스에 대한 참조를 하고 있지 않지만 Person
인스턴스와 Apartment
인스턴스의 변수가 각각 상호 참조를 하고 있어 참조 횟수가 1이기 때문에 이 두 인스턴스는 해지되지 않고 메모리 누수가 발생한다.
앞에서 살펴본 강한 순환 참조 문제를 해결하기 위해서는 두가지 방법이 있다. 하나는 weak
참조, 다른 하나는 unowned
참조를 사용하는 것이다. weak
참조와 unowned
참조 모두 ARC에서 참조 횟수를 증가시키지 않고 인스턴스를 참조하기 때문에 강한 순환 참조 문제를 해결할 수 있다. weak
참조는 참조하고 있는 인스턴스가 먼저 메모리에서 해제될 때 사용하고 unowned
참조는 반대로 참조하고 있는 인스턴스가 같은 시점 혹은 더 뒤에 해제될 때 사용한다. 위 Apartment
예제에서 Apartment
의 tenant
는 없는 상태가 될 수 있기 때문에 weak
참조를 사용하는 것이 적절하다.
weak
참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에 ARC는 weak
참조로 선언된 참조 대상이 해지 되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당한다.
예제를 보면 Apartment
의 tenant
변수는 weak
으로 선언됐다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
그리고 앞서 예제처럼 Person
인스턴스와 Apartment
인스턴스의 변수에서 각각 인스턴스를 상호 참조하도록 할당한다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
그러면 아래와 같은 참조 상황이 된다. 앞선 예제와 다른 점은 Apartment
의 tenant
변수가 Person
인스턴스를 weak
참조하고 있다는 점이다. 그래서 이 시점에서 Person
인스턴스에 대한 참조 횟수는 변수 john
이 참조하고 있는 1회 뿐이다.
그래서 john
의 참조 대상을 nil로 할당하면 더이상 Person
인스턴스를 참조하는 것이 없게 된다.
john = nil
// Prints "John Appleseed is being deinitialized"
그 결과 ARC에서 아래 그림과 같이 Person
인스턴스를 메모리에서 해지한다.
이 시점에 unit4A
에 nil을 할당하면 Apartment
인스턴스를 참조하는 개체도 사라지게 되서 Apartment
인스턴스도 메모리에서 해지된다.
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
unowned
참조는. weak
참조와 다르게 참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 lifetime을 갖거나 더 긴 lifetime을 갖기 때문에 항상 참조에 그 값이 있다고 기대된다. 그래서 ARC는 unowned
참조에는 절대 nil을 할당하지 않는다. 다시 말하면 unowned
참조는 옵셔널 타입을 사용하지 않는다.
예제를 통해 unowned
참조의 동작을 확인해 보자. 우선 Customer
와 CreditCard
두 개의 클래스를 선언한다.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
여기서 Customer
는 CreditCard
인스턴스를 참조하고 있고 CreditCard
는 customer
로 Customer
인스턴스를 참조하고 있다. customer
은 unowned
참조로 선언한다. 이유는 고객과 신용카드를 비교했을 때 신용카드는 없더라도 사용자는 남아있을 것이기 때문이다. 그래서 CreditCard
에 customer
를 unowned
로 선언한다.
이제 고객 변수 john
을 옵셔널 타입으로 선언한다.
var john: Customer?
선언한 고객에 인스턴스를 생성하고 고객의 카드 변수에도 카드 인스턴스를 생성해 할당한다.
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
이 시점에서의 참조 상황을 그림으로 표현하면 다음과 같다. john
이 Customer
인스턴스를 참조하고 있고 CreditCard
인스턴스도 Customer
인스턴스를 참조하고 있지만 unowned
참조를 하고 있기 때문에 Customer
인스턴스에 대한 참조 횟수는 1회가 된다.
이 상황에서 john
변수의 Customer
인스턴스 참조를 끊으면 다음 그림과 같이 된다.
그러면 더이상 Customer
인스턴스를 강하게 참조하고 있는 인스턴스가 없으므로 Customer
인스턴스가 해제되고 인스턴스가 해제됨에 따라 CreditCard
인스턴스를 참조하고 있는 개체도 사라지므로 CreditCard
인스턴스도 메모리에서 해제된다.
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
weak
참조, unowned
참조의 구분을 해당 참조가 nil이 될 수 있느냐 없느냐로 구분할 수 있다. 하지만 이 두 경우를 제외한 제 3의 경우도 발생할 수 있다. 두 프로퍼티가 항상 값을 갖지만 한번 초기화 되면 절대 nil이 되지 않는 경우이다. 이 경우에는 unowned
프로퍼티를 암시적으로 옵셔널 프로퍼티 언래핑을 사용해 참조 문제를 해결할 수 있다.
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
Country
의 capitalCity
는 초기화 단계에서 City
클래스에 초기화 된 후 사용되게 된다. 즉 실제로 Country
의 capitalCity
는 옵셔널이 돼야 맞다. 하지만 여기서는 느낌표 연산자(!)를 이용해 명시적으로 강제 언래핑을 시켰다. 그래서 암시적 언래핑이 돼서 Country
에서 name
이 초기화 되는 시점에 self를 사용할 수 있게 된다. 그리고 City
에서는 강한 순환 참조을 피하기 위해 unowned
참조로 country
를 선언해서 두 인스턴스를 문제없이 사용할 수 있다.
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
강한 순환 참조는 변수 뿐만 아니라 클로저와 관계돼서 발생할 수도 있다. 왜냐하면 클로저에서는 self를 캡쳐하기 때문이다. 이 문제를 해결하기 위해서는 클로저 캡쳐 리스트를 사용한다. 예제를 보면 아래 HTMLElement
클래스의 클로저 asHTML
는 입력값을 받지 않고 반환값이 String인 () -> String
클로저를 사용한다. 그리고 이 클로저 안에서 self.text
와 self.name
과 같이 self를 캡쳐하게 된다.
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
asHTML
클로저는 아래와 같이 다른 클로저로 변경 될 수도 있다.
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
이 코드를 실행하면 결과는 다음과 같다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
아래와 같이 인스턴스와 클로저 간에 강한 참조를 하게 돼서 강한 순환 참조에 빠지게 된다.
paragraph
의 참조를 nil로 할당하더라도 HTMLElement
인스턴스는 해제되지 않는다.
paragraph = nil
클로저에서 강한 순환 참조 문제를 해결하기 위해서 캡쳐 참조에 강한 참조 weak
참조 혹은 unowned
참조를 지정할 수 있다. weak
참조인지 unowned
참조를 사용할지는 코드에서 상호 관계에 달려있다.
캡처리스트를 정의하기 위해서는 클로저의 파라미터 앞에 대괄호 []
를 넣고 그 안에 각 캡쳐 대상에 대한 참조 타입을 적어준다.
lazy var someClosure: (Int, String) -> String {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
클로저의 파라미터가 없고 반환값이 추론에 의해 생략 가능한 경우에는 캡쳐리스트 정의를 in 앞에 적어준다.
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
앞서 인스턴스 참조와 마찬가지로 참조가 먼저 해제되는 경우는 weak
참조를 같은 시점이나 나중 시점에 해제되는 경우에는 unowned
참조를 사용한다.
asHTML
클로저의 self에 [unowned self]
라고 캡쳐리스트를 아래 코드와 같이 적어준다.
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
앞에서와 같이 인스턴스를 생성해 실행한다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
참조 상황을 그림으로 살펴보면 앞에서와 다르게 클로저에서 HTMLElement
인스턴스를 unowned
참조로 참조하고 있다.
그래서 paragaph
의 참조를 제거하면 HTMLElement
인스턴스가 바로 메모리에서 해제되는 것을 확인할 수 있다.
paragraph = nil
// Prints "p is being deinitialized"