자동 참조 카운팅
자동 참조 카운팅 (Automatic Reference Counting)
객체의 수명과 객체 간의 관계를 모델링하는 방법을 알아봐요.
Swift는 자동 참조 카운팅(Automatic Reference Counting, ARC)을 사용해서 앱의 메모리 사용량을 추적하고 관리해요. 대부분의 경우 이 덕분에 Swift에서는 메모리 관리를 직접 신경 쓸 필요 없이 "저절로" 동작해요. ARC는 클래스 인스턴스가 더 이상 필요 없어지면 그 인스턴스가 사용하던 메모리를 자동으로 해제해 줘요.
다만 몇몇 경우에는 ARC가 메모리를 관리해 줄 수 있도록 코드의 각 부분이 맺고 있는 관계에 대한 정보를 더 요구하기도 해요. 이번 챕터에서는 그런 상황들을 설명하고, ARC가 앱의 모든 메모리를 관리하도록 만드는 방법을 보여드릴게요. Swift에서 ARC를 사용하는 방식은 Objective-C에서 ARC를 사용하는 방식과 매우 비슷해요.
참조 카운팅은 클래스의 인스턴스에만 적용돼요. 구조체와 열거형은 값 타입이지 참조 타입이 아니기 때문에, 참조로 저장되거나 전달되지 않아요.
본문
ARC는 어떻게 동작하나요?
클래스의 새 인스턴스를 만들 때마다 ARC는 그 인스턴스에 관한 정보를 저장할 메모리 블록 하나를 할당해요. 그 메모리에는 인스턴스의 타입 정보와, 그 인스턴스에 연결된 저장 프로퍼티들의 값이 함께 담겨요.
또한 인스턴스가 더 이상 필요 없어지면 ARC는 그 인스턴스가 사용하던 메모리를 해제해서, 그 메모리를 다른 용도로 쓸 수 있게 만들어 줘요. 그래서 클래스 인스턴스가 더 이상 필요 없을 때 메모리 공간을 차지하지 않게 되는 거죠.
하지만 만약 ARC가 아직 사용 중인 인스턴스를 해제해 버리면, 그 인스턴스의 프로퍼티에 접근하거나 메서드를 호출할 수 없게 돼요. 실제로 그 인스턴스에 접근하려고 하면 앱이 대부분 크래시할 거예요.
인스턴스가 여전히 필요한 동안 사라지지 않도록, ARC는 각 클래스 인스턴스를 현재 몇 개의 프로퍼티와 상수, 변수가 참조하고 있는지 추적해요. ARC는 해당 인스턴스에 대한 활성 참조가 하나라도 남아 있는 한 그 인스턴스를 해제하지 않아요.
이게 가능하려면, 클래스 인스턴스를 프로퍼티·상수·변수에 할당할 때마다 그 프로퍼티·상수·변수가 그 인스턴스에 강한 참조(strong reference)를 만든다고 해요. 그 참조가 남아 있는 동안에는 인스턴스가 해제되지 않도록 단단히 붙들고 있기 때문에 "강한" 참조라고 불러요.
ARC가 실제로 동작하는 모습
자동 참조 카운팅이 어떻게 동작하는지 예시로 살펴볼게요. 먼저 name이라는 저장 상수 프로퍼티를 가진 간단한 Person 클래스부터 시작해요.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Person 클래스에는 인스턴스의 name 프로퍼티를 설정하고, 초기화가 진행 중임을 알리는 메시지를 출력하는 초기화 메서드(initializer)가 있어요. 또 인스턴스가 해제될 때 메시지를 출력하는 소멸 메서드(deinitializer)도 있어요.
다음 코드 조각은 Person? 타입의 변수 세 개를 정의해요. 이 변수들은 뒤따르는 코드에서 새 Person 인스턴스를 가리키는 여러 참조를 만들 때 사용돼요. 이 변수들은 옵셔널 타입(Person이 아니라 Person?)이기 때문에 자동으로 nil 값으로 초기화되어서, 현재는 어떤 Person 인스턴스도 참조하지 않아요.
var reference1: Person?
var reference2: Person?
var reference3: Person?
이제 새 Person 인스턴스를 만들어서 이 변수 세 개 중 하나에 할당할 수 있어요.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized".
"John Appleseed is being initialized"이라는 메시지가 Person 클래스의 초기화 메서드를 호출하는 시점에 출력된다는 점을 눈여겨보세요. 이로써 초기화가 일어났음을 확인할 수 있어요.
새 Person 인스턴스가 reference1 변수에 할당됐기 때문에, 이제 reference1에서 새 Person 인스턴스로의 강한 참조가 하나 생겼어요. 강한 참조가 하나 이상 있으므로 ARC는 이 Person이 메모리에 유지되고 해제되지 않도록 보장해요.
같은 Person 인스턴스를 변수 두 개에 더 할당하면, 그 인스턴스에 대한 강한 참조가 두 개 더 생겨요.
reference2 = reference1
reference3 = reference1
이제 이 단일 Person 인스턴스에 대한 강한 참조가 세 개가 됐어요.
원래 참조를 포함한 강한 참조 두 개를, 그 변수 두 개에 nil을 할당함으로써 끊으면 강한 참조 하나만 남고 Person 인스턴스는 해제되지 않아요.
reference1 = nil
reference2 = nil
ARC는 세 번째이자 마지막 강한 참조가 끊어지기 전까지는 Person 인스턴스를 해제하지 않아요. 그 시점이 되어서야 더 이상 Person 인스턴스를 사용하지 않는 것이 분명해지는 거죠.
reference3 = nil
// Prints "John Appleseed is being deinitialized".
클래스 인스턴스 사이의 강한 참조 순환
위 예시들에서 ARC는 사용자가 만든 새 Person 인스턴스에 대한 참조 수를 추적하고, 더 이상 필요 없어지면 그 Person 인스턴스를 해제할 수 있었어요.
하지만 강한 참조가 결코 0이 되지 않는 코드를 작성하는 것도 가능해요. 클래스 인스턴스 두 개가 서로에 대해 강한 참조를 붙들고 있어서, 각 인스턴스가 상대를 계속 살려 두는 경우가 그렇죠. 이를 강한 참조 순환(strong reference cycle)이라고 불러요.
강한 참조 순환은 클래스 사이의 관계 중 일부를 강한 참조 대신 weak 또는 unowned 참조로 정의하면 해결할 수 있어요. 그 과정은 뒤에서 설명할게요. 다만 해결 방법을 배우기 전에, 그런 순환이 어떤 과정으로 생기는지 먼저 이해해 두는 게 도움이 돼요.
여기 강한 참조 순환이 실수로 생겨날 수 있는 예시가 있어요. 아파트 단지와 그 거주자를 모델링하는 Person과 Apartment라는 클래스 두 개를 정의해요.
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 인스턴스는 String 타입의 name 프로퍼티와, 처음에 nil인 옵셔널 apartment 프로퍼티를 가져요. apartment 프로퍼티가 옵셔널인 이유는, 사람이 항상 아파트를 갖고 있지는 않기 때문이에요.
마찬가지로 모든 Apartment 인스턴스는 String 타입의 unit 프로퍼티와, 처음에 nil인 옵셔널 tenant 프로퍼티를 가져요. tenant 프로퍼티가 옵셔널인 이유는, 아파트에 항상 세입자가 있는 건 아니기 때문이에요.
이 두 클래스 모두 해당 클래스의 인스턴스가 해제되고 있다는 사실을 출력하는 소멸 메서드도 정의해요. 이 덕분에 Person과 Apartment 인스턴스가 예상대로 해제되고 있는지 확인할 수 있어요.
다음 코드 조각은 john과 unit4A라는 옵셔널 타입의 변수 두 개를 정의해요. 아래에서 특정 Apartment와 Person 인스턴스로 설정될 변수들이죠. 이 두 변수는 옵셔널이기 때문에 초기값이 nil이에요.
var john: Person?
var unit4A: Apartment?
이제 특정 Person 인스턴스와 Apartment 인스턴스를 만들어서 john과 unit4A 변수에 할당할 수 있어요.
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
이 두 인스턴스를 만들고 할당한 뒤의 강한 참조 모습은 이렇습니다. 이제 john 변수는 새 Person 인스턴스에 대한 강한 참조를, unit4A 변수는 새 Apartment 인스턴스에 대한 강한 참조를 가져요.
이제 두 인스턴스를 서로 연결해서, 이 사람이 아파트를 갖고 아파트에 세입자가 있게 만들 수 있어요. john과 unit4A 옵셔널 변수 안에 저장된 인스턴스를 풀어서(unwrap) 접근해 프로퍼티를 설정하려면 느낌표(!)를 사용한다는 점에 주목하세요.
john!.apartment = unit4A
unit4A!.tenant = john
두 인스턴스를 연결한 뒤의 강한 참조 모습은 이렇습니다.
아쉽게도 이렇게 두 인스턴스를 연결하면 두 인스턴스 사이에 강한 참조 순환이 생겨요. 이제 Person 인스턴스는 Apartment 인스턴스에 대한 강한 참조를, Apartment 인스턴스는 Person 인스턴스에 대한 강한 참조를 갖게 되죠. 따라서 john과 unit4A 변수가 들고 있는 강한 참조를 끊더라도 참조 카운트가 0으로 떨어지지 않아서, ARC가 인스턴스를 해제하지 못해요.
john = nil
unit4A = nil
이 두 변수에 nil을 설정했을 때 소멸 메서드가 하나도 호출되지 않았다는 점을 주목하세요. 강한 참조 순환이 Person과 Apartment 인스턴스가 해제되는 것을 막아서, 앱에 메모리 누수(memory leak)를 일으키는 거죠.
john과 unit4A 변수에 nil을 설정한 뒤의 강한 참조 모습은 이렇습니다. Person 인스턴스와 Apartment 인스턴스 사이의 강한 참조는 그대로 남아 있고, 끊을 수가 없어요.
클래스 인스턴스 사이의 강한 참조 순환 해결하기
클래스 타입의 프로퍼티를 다룰 때 강한 참조 순환을 해결하는 방법은 두 가지가 있어요. 바로 weak 참조와 unowned 참조예요.
weak와 unowned 참조는 참조 순환 안에 있는 한 인스턴스가 다른 인스턴스를 강하게 붙들지 않고 가리키게 해 줘요. 그러면 두 인스턴스가 강한 참조 순환을 만들지 않고 서로를 참조할 수 있게 돼요.
다른 인스턴스의 수명이 더 짧을 때, 즉 상대 인스턴스가 먼저 해제될 수 있을 때는 weak 참조를 사용해요. 위 Apartment 예시에서 아파트는 수명 중 어느 시점에는 세입자가 없을 수 있는 게 자연스럽기 때문에, 이 경우 참조 순환을 끊는 방법으로 weak 참조가 적절해요. 반면 다른 인스턴스의 수명이 같거나 더 길 때는 unowned 참조를 사용해요.
Weak 참조
weak 참조는 자신이 참조하는 인스턴스를 강하게 붙들지 않는 참조라서, 참조된 인스턴스를 ARC가 처리하는 것을 막지 않아요. 이 동작 덕분에 weak 참조가 강한 참조 순환의 일부가 되지 않아요. weak 참조는 프로퍼티나 변수 선언 앞에 weak 키워드를 붙여서 표시해요.
weak 참조가 참조하는 인스턴스를 강하게 붙들지 않기 때문에, weak 참조가 여전히 그 인스턴스를 가리키고 있는 동안 그 인스턴스가 해제될 수도 있어요. 그래서 ARC는 weak 참조가 가리키던 인스턴스가 해제되면 그 weak 참조를 자동으로 nil로 설정해요. 또 weak 참조는 런타임에 자신의 값을 nil로 바꿀 수 있어야 하기 때문에, 항상 상수가 아니라 변수의 옵셔널 타입으로 선언돼요.
다른 옵셔널 값처럼 weak 참조에 값이 있는지 확인할 수 있고, 더 이상 존재하지 않는 유효하지 않은 인스턴스에 대한 참조를 갖게 되는 일은 절대 없어요.
Note: ARC가 weak 참조를
nil로 설정할 때는 프로퍼티 관찰자(property observer)가 호출되지 않아요.
아래 예시는 위의 Person과 Apartment 예시와 동일한데, 중요한 차이 하나가 있어요. 이번에는 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") }
}
두 변수(john과 unit4A)에서 오는 강한 참조와, 두 인스턴스 사이의 연결은 이전과 같은 방식으로 만들어져요.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
이제 두 인스턴스를 연결한 뒤의 참조 모습은 이렇습니다. Person 인스턴스는 여전히 Apartment 인스턴스에 대한 강한 참조를 갖지만, Apartment 인스턴스는 이제 Person 인스턴스에 대한 weak 참조를 가져요. 이 말은, john 변수에 nil을 설정해서 그 변수가 들고 있던 강한 참조를 끊으면 Person 인스턴스에 대한 강한 참조가 하나도 남지 않게 된다는 뜻이에요.
john = nil
// Prints "John Appleseed is being deinitialized".
Person 인스턴스에 대한 강한 참조가 하나도 남지 않았기 때문에 그 인스턴스는 해제되고, tenant 프로퍼티는 nil로 설정돼요.
Apartment 인스턴스에 대한 남은 유일한 강한 참조는 unit4A 변수에서 오는 거예요. 그 강한 참조를 끊으면 Apartment 인스턴스에 대한 강한 참조가 하나도 남지 않게 돼요.
unit4A = nil
// Prints "Apartment 4A is being deinitialized".
Apartment 인스턴스에 대한 강한 참조가 하나도 남지 않았으므로 그 인스턴스도 해제돼요.
Note: 가비지 컬렉션을 사용하는 시스템에서는 때때로 weak 포인터를 사용해 간단한 캐싱 메커니즘을 구현하기도 해요. 강한 참조가 없는 객체들은 메모리 압박이 가비지 컬렉션을 촉발할 때에만 해제되기 때문이죠. 하지만 ARC에서는 값이 마지막 강한 참조가 제거되는 즉시 해제되기 때문에, 그런 용도에는 weak 참조가 적합하지 않아요.
Unowned 참조
weak 참조와 마찬가지로 unowned 참조도 자신이 참조하는 인스턴스를 강하게 붙들지 않아요. 다만 weak 참조와 달리 unowned 참조는 상대 인스턴스의 수명이 같거나 더 길 때 사용해요. unowned 참조는 프로퍼티나 변수 선언 앞에 unowned 키워드를 붙여서 표시해요.
weak 참조와 달리 unowned 참조는 항상 값을 가지고 있을 것으로 기대돼요. 그 결과 값을 unowned로 표시해도 옵셔널이 되지 않고, ARC가 unowned 참조의 값을 nil로 설정하는 일도 절대 없어요.
Important: unowned 참조는 참조가 항상 아직 해제되지 않은 인스턴스를 가리킨다고 확신할 수 있을 때만 사용해야 해요.
인스턴스가 해제된 뒤에 unowned 참조의 값에 접근하려고 하면 런타임 오류가 발생해요.
다음 예시는 은행 고객과 그 고객이 가질 수 있는 신용카드를 모델링하는 Customer와 CreditCard 클래스 두 개를 정의해요. 이 두 클래스는 각각 다른 클래스의 인스턴스를 프로퍼티로 저장해요. 이 관계는 강한 참조 순환을 만들 가능성이 있어요.
Customer와 CreditCard의 관계는 위 weak 참조 예시에서 본 Apartment와 Person의 관계와는 조금 달라요. 이 데이터 모델에서 고객은 신용카드를 가질 수도 있고 안 가질 수도 있지만, 신용카드는 항상 고객과 연결돼 있어요. CreditCard 인스턴스는 자신이 가리키는 Customer보다 오래 살지 않아요. 이를 나타내기 위해 Customer 클래스는 옵셔널 card 프로퍼티를 갖지만, CreditCard 클래스는 unowned(그리고 비옵셔널) customer 프로퍼티를 가져요.
또한 새 CreditCard 인스턴스는 오직 number 값과 customer 인스턴스를 커스텀 CreditCard 초기화 메서드에 전달함으로써만 만들 수 있어요. 이렇게 해서 CreditCard 인스턴스가 만들어질 때 항상 그와 연결된 customer 인스턴스를 갖게 되는 것을 보장해요.
신용카드는 항상 고객을 갖기 때문에 강한 참조 순환을 피하려고 customer 프로퍼티를 unowned 참조로 정의해요.
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") }
}
Note:
CreditCard클래스의number프로퍼티는 32비트와 64비트 시스템 모두에서 16자리 카드 번호를 저장할 만큼 충분한 용량을 갖도록Int가 아니라UInt64타입으로 정의돼요.
다음 코드 조각은 특정 고객에 대한 참조를 저장하는 데 쓰일 옵셔널 Customer 변수 john을 정의해요. 이 변수는 옵셔널이기 때문에 초기값이 nil이에요.
var john: Customer?
이제 Customer 인스턴스를 만들고, 새 CreditCard 인스턴스를 그 고객의 card 프로퍼티로 초기화해서 할당할 수 있어요.
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
두 인스턴스를 연결한 뒤의 참조 모습은 이렇습니다. Customer 인스턴스는 이제 CreditCard 인스턴스에 대한 강한 참조를 갖고, CreditCard 인스턴스는 Customer 인스턴스에 대한 unowned 참조를 가져요.
unowned customer 참조 덕분에 john 변수가 들고 있던 강한 참조를 끊으면 Customer 인스턴스에 대한 강한 참조가 하나도 남지 않게 돼요.
Customer 인스턴스에 대한 강한 참조가 하나도 남지 않았으므로 그 인스턴스는 해제돼요. 그 일이 일어난 뒤에는 CreditCard 인스턴스에 대한 강한 참조도 하나도 남지 않아서, 그 인스턴스도 해제돼요.
john = nil
// Prints "John Appleseed is being deinitialized".
// Prints "Card #1234567890123456 is being deinitialized".
위의 마지막 코드 조각은 john 변수에 nil을 설정한 뒤 Customer 인스턴스와 CreditCard 인스턴스의 소멸 메서드가 모두 자신의 "deinitialized" 메시지를 출력하는 것을 보여줘요.
Note: 위 예시들은 안전한(safe) unowned 참조를 사용하는 방법을 보여줘요. Swift는 런타임 안전 검사를 꺼야 하는 경우(예를 들어 성능상 이유로)를 위해 안전하지 않은(unsafe) unowned 참조도 제공해요. 다른 모든 안전하지 않은 연산과 마찬가지로, 그 코드의 안전성을 검사할 책임은 여러분에게 있어요.
안전하지 않은 unowned 참조는
unowned(unsafe)라고 써서 표시해요. 안전하지 않은 unowned 참조가 가리키던 인스턴스가 해제된 뒤에 그 참조에 접근하려고 하면, 프로그램이 인스턴스가 예전에 있던 메모리 위치에 접근하려고 시도하는데, 이는 안전하지 않은 연산이에요.
Unowned 옵셔널 참조
클래스에 대한 옵셔널 참조를 unowned로 표시할 수 있어요. ARC 소유권 모델 관점에서 unowned 옵셔널 참조와 weak 참조는 같은 문맥에서 사용될 수 있어요. 차이는 unowned 옵셔널 참조를 사용할 때는, 그것이 항상 유효한 객체를 가리키거나 nil로 설정되도록 하는 책임이 여러분에게 있다는 점이에요.
여기 한 학교의 특정 학과가 제공하는 강좌들을 추적하는 예시가 있어요.
class Department {
var name: String
var courses: [Course]
init(name: String) {
self.name = name
self.courses = []
}
}
class Course {
var name: String
unowned var department: Department
unowned var nextCourse: Course?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextCourse = nil
}
}
Department는 학과가 제공하는 각 강좌에 대한 강한 참조를 유지해요. ARC 소유권 모델에서 학과는 자기 강좌들을 소유해요. Course는 학과에 대한 참조 하나와, 학생이 다음에 들어야 할 강좌에 대한 참조 하나, 모두 unowned 참조 두 개를 가져요. 강좌는 이 두 객체를 둘 다 소유하지 않아요. 모든 강좌는 어떤 학과에 속해 있으므로 department 프로퍼티는 옵셔널이 아니에요. 하지만 일부 강좌에는 권장되는 후속 강좌가 없을 수 있기 때문에 nextCourse 프로퍼티는 옵셔널이에요.
이 클래스들을 사용하는 예시는 이렇습니다.
let department = Department(name: "Horticulture")
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
위 코드는 학과 하나와 강좌 세 개를 만들어요. intro와 intermediate 강좌에는 모두 권장되는 후속 강좌가 nextCourse 프로퍼티에 저장되어 있어요. 이 프로퍼티는 이 강좌를 수강한 뒤 학생이 들어야 할 강좌에 대한 unowned 옵셔널 참조를 유지해요.
unowned 옵셔널 참조는 자신이 감싸는 클래스 인스턴스를 강하게 붙들지 않기 때문에, ARC가 그 인스턴스를 해제하는 것을 막지 않아요. 이는 ARC 아래에서 unowned 참조와 동일하게 동작하는데, 다만 unowned 옵셔널 참조는 nil이 될 수 있다는 점만 달라요.
비옵셔널 unowned 참조와 마찬가지로, nextCourse가 항상 아직 해제되지 않은 강좌를 가리키도록 보장하는 책임은 여러분에게 있어요. 이 경우 예를 들어 department.courses에서 강좌 하나를 삭제할 때, 다른 강좌들이 그 강좌에 대해 갖고 있을 수 있는 참조도 함께 제거해 줘야 해요.
Note: 옵셔널 값의 밑바탕 타입은 Swift 표준 라이브러리의 열거형인
Optional이에요. 그런데 값 타입에는unowned를 붙일 수 없다는 규칙의 예외가 옵셔널이에요.클래스를 감싸는 옵셔널은 참조 카운팅을 사용하지 않기 때문에, 옵셔널에 대한 강한 참조를 유지할 필요가 없어요.
Unowned 참조와 암시적 언래핑 옵셔널 프로퍼티
위의 weak 참조와 unowned 참조 예시들은 강한 참조 순환을 끊는 데 필요한 두 가지 더 흔한 시나리오를 다뤄요.
Person과 Apartment 예시는 둘 다 nil이 될 수 있는 프로퍼티 두 개가 강한 참조 순환을 일으킬 가능성이 있는 상황을 보여줘요. 이 시나리오는 weak 참조로 해결하는 게 가장 좋아요.
Customer와 CreditCard 예시는 하나는 nil이 될 수 있고 다른 하나는 nil이 될 수 없는 프로퍼티가 강한 참조 순환을 일으킬 가능성이 있는 상황을 보여줘요. 이 시나리오는 unowned 참조로 해결하는 게 가장 좋아요.
하지만 세 번째 시나리오가 있는데, 두 프로퍼티 모두 항상 값을 가져야 하고, 초기화가 끝난 뒤에는 어느 쪽도 nil이 되면 안 되는 경우예요. 이 시나리오에서는 한 클래스에 unowned 프로퍼티를, 다른 클래스에 암시적 언래핑 옵셔널(implicitly unwrapped optional) 프로퍼티를 결합해서 쓰는 게 유용해요.
이렇게 하면 참조 순환을 피하면서도, 초기화가 끝난 뒤 두 프로퍼티 모두 (옵셔널 언래핑 없이) 직접 접근할 수 있어요. 이 절에서는 그런 관계를 설정하는 방법을 보여드릴게요.
아래 예시는 Country와 City 클래스 두 개를 정의하며, 각 클래스는 다른 클래스의 인스턴스를 프로퍼티로 저장해요. 이 데이터 모델에서 모든 나라는 반드시 수도(capital city)를 가져야 하고, 모든 도시는 반드시 어떤 나라에 속해야 해요. 이를 나타내기 위해 Country 클래스는 capitalCity 프로퍼티를, City 클래스는 country 프로퍼티를 가져요.
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
}
}
두 클래스 사이의 상호 의존성을 설정하기 위해 City의 초기화 메서드는 Country 인스턴스를 받아서 그 인스턴스를 country 프로퍼티에 저장해요.
City의 초기화 메서드는 Country의 초기화 메서드 안에서 호출돼요. 그런데 Country의 초기화 메서드는 새 Country 인스턴스가 완전히 초기화되기 전까지는 City 초기화 메서드에 self를 전달할 수 없어요.
이 요구 사항을 처리하기 위해 Country의 capitalCity 프로퍼티를 암시적 언래핑 옵셔널 프로퍼티로 선언해요. 이는 타입 어노테이션 끝의 느낌표(City!)로 표시돼요. 이 말은 capitalCity 프로퍼티가 다른 옵셔널과 마찬가지로 기본값 nil을 갖지만, 값을 언래핑하지 않고도 접근할 수 있다는 뜻이에요.
capitalCity가 기본값 nil을 갖기 때문에, Country 인스턴스가 초기화 메서드 안에서 name 프로퍼티를 설정하는 즉시 새 Country 인스턴스는 완전히 초기화된 것으로 간주돼요. 이 말은 Country 초기화 메서드가 name 프로퍼티가 설정되는 즉시 암시적 self 프로퍼티를 참조하고 전달하기 시작할 수 있다는 뜻이에요. 따라서 Country 초기화 메서드는 자신의 capitalCity 프로퍼티를 설정할 때 self를 City 초기화 메서드의 매개 변수 중 하나로 전달할 수 있어요.
이 모든 것은 강한 참조 순환을 만들지 않고도 Country와 City 인스턴스를 단일 문장으로 만들 수 있고, 옵셔널 값을 언래핑하려고 느낌표를 쓸 필요 없이 capitalCity 프로퍼티에 직접 접근할 수 있다는 뜻이에요.
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".
위 예시에서 암시적 언래핑 옵셔널을 사용함으로써 클래스 초기화 메서드의 모든 2단계 요구 사항이 충족돼요. capitalCity 프로퍼티는 초기화가 끝나면 비옵셔널 값처럼 사용하고 접근할 수 있으면서도, 강한 참조 순환을 피할 수 있어요.
클로저의 강한 참조 순환
클래스 인스턴스의 프로퍼티 두 개가 서로에 대해 강한 참조를 붙들 때 강한 참조 순환이 어떻게 생기는지 위에서 봤어요. 또 weak와 unowned 참조를 사용해 그런 강한 참조 순환을 끊는 방법도 봤죠.
클래스 인스턴스의 프로퍼티에 클로저를 할당하고, 그 클로저의 본문이 그 인스턴스를 캡처하면 강한 참조 순환도 생길 수 있어요. 이 캡처는 클로저 본문이 인스턴스의 프로퍼티에 접근하거나(self.someProperty 같은), 클로저가 인스턴스의 메서드를 호출할 때(self.someMethod() 같은) 일어날 수 있어요. 어느 쪽이든 이런 접근은 클로저가 self를 "캡처"하게 만들어 강한 참조 순환을 만들게 돼요.
이 강한 참조 순환은 클로저가 클래스처럼 참조 타입이기 때문에 생겨요. 클로저를 프로퍼티에 할당하면 그 클로저에 대한 참조를 할당하는 거죠. 본질적으로 위와 같은 문제예요. 강한 참조 두 개가 서로를 계속 살려 두는 거죠. 다만 이번에는 클래스 인스턴스 두 개가 아니라, 클래스 인스턴스와 클로저가 서로를 살려 두는 거예요.
Swift는 이 문제에 대한 우아한 해결책을 제공하는데, 바로 클로저 캡처 목록(closure capture list)이라고 불러요. 다만 클로저 캡처 목록으로 강한 참조 순환을 끊는 방법을 배우기 전에, 그런 순환이 어떻게 생기는지 이해해 두는 게 유용해요.
아래 예시는 self를 참조하는 클로저를 사용할 때 강한 참조 순환을 어떻게 만들 수 있는지 보여줘요. 이 예시는 HTML 문서 안의 개별 요소에 대한 간단한 모델을 제공하는 HTMLElement라는 클래스를 정의해요.
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")
}
}
HTMLElement 클래스는 요소의 이름을 나타내는 name 프로퍼티를 정의해요. 헤딩 요소라면 "h1", 문단 요소라면 "p", 줄바꿈 요소라면 "br" 같은 값이죠. HTMLElement는 또 그 HTML 요소 안에 렌더링될 텍스트를 나타내는 문자열로 설정할 수 있는 옵셔널 text 프로퍼티도 정의해요.
이 두 단순한 프로퍼티에 더해, HTMLElement 클래스는 asHTML이라는 lazy 프로퍼티를 정의해요. 이 프로퍼티는 name과 text를 HTML 문자열 조각으로 합치는 클로저를 참조해요. asHTML 프로퍼티의 타입은 () -> String, 즉 "매개 변수를 받지 않고 String 값을 반환하는 함수"예요.
기본적으로 asHTML 프로퍼티에는 HTML 태그의 문자열 표현을 반환하는 클로저가 할당돼요. 이 태그는 옵셔널 text 값이 있으면 그 값을 포함하고, text가 없으면 텍스트 내용이 없어요. 문단 요소라면 클로저는 text 프로퍼티가 "some text"인지 nil인지에 따라 "<p>some text</p>" 또는 "<p />"를 반환해요.
asHTML 프로퍼티는 인스턴스 메서드와 비슷하게 이름 붙고 사용돼요. 다만 asHTML은 인스턴스 메서드가 아니라 클로저 프로퍼티이기 때문에, 특정 HTML 요소의 렌더링을 바꾸고 싶다면 asHTML 프로퍼티의 기본값을 커스텀 클로저로 교체할 수 있어요.
예를 들어 text 프로퍼티가 nil일 때 기본 텍스트로 대체하는 클로저를 asHTML 프로퍼티에 설정하면, 표현이 빈 HTML 태그를 반환하는 것을 막을 수 있어요.
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>".
Note:
asHTML프로퍼티는 lazy 프로퍼티로 선언돼요. 요소가 실제로 어떤 HTML 출력 대상에 문자열 값으로 렌더링되어야 할 때에만 필요하기 때문이에요.asHTML이 lazy 프로퍼티라는 사실 덕분에 기본 클로저 안에서self를 참조할 수 있어요. lazy 프로퍼티는 초기화가 완료되고self가 존재한다는 것이 확인된 뒤에야 접근되기 때문이죠.
HTMLElement 클래스는 새 요소를 초기화하기 위해 name 인자와 (원한다면) text 인자를 받는 초기화 메서드 하나를 제공해요. 이 클래스는 또 HTMLElement 인스턴스가 해제될 때를 보여 주는 메시지를 출력하는 소멸 메서드도 정의해요.
HTMLElement 클래스를 사용해서 새 인스턴스를 만들고 출력하는 방법은 이렇습니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>".
Note: 위
paragraph변수는 아래에서nil로 설정해서 강한 참조 순환의 존재를 보여 줄 수 있도록 옵셔널HTMLElement로 정의되어 있어요.
아쉽게도 위와 같이 작성된 HTMLElement 클래스는 HTMLElement 인스턴스와 기본 asHTML 값에 쓰이는 클로저 사이에 강한 참조 순환을 만들어요.
인스턴스의 asHTML 프로퍼티는 자신의 클로저에 대한 강한 참조를 붙들고 있어요. 그런데 클로저는 본문 안에서 self를 참조하므로(self.name, self.text를 참조하는 방식으로) 클로저는 self를 캡처해요. 즉 HTMLElement 인스턴스로 되돌아오는 강한 참조를 붙든다는 뜻이죠. 이 둘 사이에 강한 참조 순환이 생기는 거예요.
Note: 클로저가
self를 여러 번 참조하더라도,HTMLElement인스턴스에 대한 강한 참조는 하나만 캡처해요.
paragraph 변수에 nil을 설정해서 HTMLElement 인스턴스에 대한 강한 참조를 끊으면, 강한 참조 순환이 HTMLElement 인스턴스와 그 클로저를 모두 해제되지 않게 막아요.
paragraph = nil
HTMLElement 소멸 메서드의 메시지가 출력되지 않는 것에 주목하세요. 이는 HTMLElement 인스턴스가 해제되지 않았다는 뜻이에요.
클로저의 강한 참조 순환 해결하기
클로저와 클래스 인스턴스 사이의 강한 참조 순환은 클로저 정의의 일부로 캡처 목록(capture list)을 정의함으로써 해결해요. 캡처 목록은 클로저 본문 안에서 하나 이상의 참조 타입을 캡처할 때 사용할 규칙을 정의해요. 클래스 인스턴스 두 개 사이의 강한 참조 순환과 마찬가지로, 캡처되는 각 참조를 강한 참조가 아니라 weak 또는 unowned 참조로 선언해요. weak인지 unowned인지의 적절한 선택은 코드의 여러 부분 사이의 관계에 달려 있어요.
Note: Swift는 클로저 안에서
self의 멤버를 참조할 때는 (그냥someProperty나someMethod()가 아니라)self.someProperty나self.someMethod()라고 쓰도록 요구해요. 이는 실수로self를 캡처할 가능성을 기억하게 도와주려는 장치예요.
캡처 목록 정의하기
캡처 목록의 각 항목은 weak 또는 unowned 키워드와, 클래스 인스턴스에 대한 참조(self 같은) 또는 어떤 값으로 초기화된 변수(delegate = self.delegate 같은)의 짝이에요. 이 짝들은 쉼표로 구분해서 대괄호 쌍 안에 써요.
캡처 목록은 클로저의 매개 변수 목록과 반환 타입이 제공된다면 그 앞에 배치해요.
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
클로저가 매개 변수 목록이나 반환 타입을 문맥에서 추론할 수 있어서 지정하지 않는다면, 캡처 목록을 클로저의 맨 앞에 놓고 in 키워드를 뒤에 붙여요.
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// closure body goes here
}
Weak와 Unowned 참조
클로저와 그것이 캡처하는 인스턴스가 항상 서로를 가리키고, 항상 동시에 해제될 때는 클로저 안의 캡처를 unowned 참조로 정의해요.
반대로 캡처된 참조가 미래의 어느 시점에 nil이 될 수 있을 때는 캡처를 weak 참조로 정의해요. weak 참조는 항상 옵셔널 타입이고, 참조하는 인스턴스가 해제되면 자동으로 nil이 돼요. 그래서 클로저 본문 안에서 그 존재를 확인할 수 있어요.
Note: 캡처된 참조가 절대
nil이 되지 않을 것이라면, weak 참조보다는 항상 unowned 참조로 캡처해야 해요.
unowned 참조는 위에서 본 HTMLElement 예시의 강한 참조 순환을 해결하는 데 적절한 캡처 방식이에요. 순환을 피하도록 HTMLElement 클래스를 작성하는 방법은 이렇습니다.
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")
}
}
이 HTMLElement 구현은 asHTML 클로저 안에 캡처 목록을 추가한 것 말고는 이전 구현과 동일해요. 이 경우 캡처 목록은 [unowned self]인데, 이는 "self를 강한 참조가 아니라 unowned 참조로 캡처한다"는 뜻이에요.
이전과 같은 방식으로 HTMLElement 인스턴스를 만들고 출력할 수 있어요.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>".
이제 캡처 목록이 제자리에 있는 참조 모습은 이렇습니다. 이번에는 클로저가 self를 캡처하는 것이 unowned 참조라서, 클로저가 캡처한 HTMLElement 인스턴스를 강하게 붙들지 않아요. paragraph 변수의 강한 참조에 nil을 설정하면 HTMLElement 인스턴스가 해제돼요. 아래 예시에서 소멸 메서드 메시지가 출력되는 것으로 확인할 수 있죠.
paragraph = nil
// Prints "p is being deinitialized".
더 알아보기
- 값 타입 vs 참조 타입: 참조 카운팅은 클래스 인스턴스에만 적용돼요. 구조체와 열거형이 값 타입으로 어떻게 동작하는지, 언제 클래스를 써야 하는지는 '구조체와 클래스' 챕터에서 다뤄요.
- 클로저와 캡처: 클로저가 값을 어떻게 캡처하는지, 캡처 목록이 어떻게 동작하는지 더 자세히 보려면 '클로저' 챕터와 '표현식' 챕터의 캡처 목록 항목을 참고하세요.
- 초기화:
Country와City예시에서 나온 2단계 초기화와 암시적 언래핑 옵셔널의 배경을 확인하려면 '초기화' 챕터와 '기초' 챕터를 참고하세요. - Objective-C에서의 ARC: ARC를 Objective-C에서 사용하는 방식과 Swift 방식의 유사점을 보려면 Transitioning to ARC Release Notes를 참고하세요.