Swift - Automatic Reference Counting(ARC)
Swift 공식 문서를 번역 및 정리한 글입니다.
ARC
Swift는 앱의 메모리 사용을 추적, 관리하기 위해 ARC를 사용한다.ARC는 클래스의 인스턴스들이 더 이상 사용되지 않을 때 자동적으로 클래스 인스턴스를 메모리를 해제시킨다.
How ARC Works
새로운 클래스 인스턴스가 생성될 때마다 ARC는 인스턴스의 정보(타입, 저장 프로퍼티의 값 등)를 저장하기 위한 메모리를 할당하고, 인스턴스가 더 이상 사용되지 않을 때 메모리에서 해제한다. 그러나, 만약 ARC가 여전히 사용 중인 인스턴스를 할당 해제(deallocate) 한다면, 더 이상 인스턴스의 프로퍼티에 접근할 수 없고 인스턴스의 메서드를 호출할 수도 없다.이와 같은 상황을 막기 위해 ARC는 현재 각 클래스 인스턴스를 참조하는 프로퍼티, 상수 및 변수의 수를 추적한다. 그리고 단 한개라도 활성화된 참조가 존재한다면 ARC는 인스턴스를 할당 해제하지 않는다.
이를 위해 ARC는 프로퍼티, 상수, 변수 등에 인스턴스를 할당 할때마다 이들이 인스턴스를 강하게 참조 하도록 한다. 이런 참조를 강한 참조(strong reference)라고 부르며, 강한 참조 가 존재하는한 인스턴스는 메모리에서 해제되지 않는다.
ARC in Action
아래의 코드를 보면 3개의 옵셔널 타입 Person?
변수가 정의되어있고, 옵셔널 타입이기 때문에 nil 값이 자동으로 초기화된다. 따라서 아직 Person 인스턴스를 참조하고 있지는 않다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\\(name) is being initailized")
}
deinit {
print("\\(name) is being deinitailized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
이제 새로운 Person
인스턴스를 생성하고 변수 중 하나에 할당한다.이로 인해 reference1
에서 Person
인스턴스로의 강한 참조가 생긴다.이제 적어도 하나의 강한 참고가 존재하기 때문에 ARC는 Person
인스턴스에 대해 RC를 1 증가시키고, 인스턴스가 메모리에서 해제되지 않게 된다.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initailized"
같은 Person 인스턴스를 남은 두 개의 변수에 할당하면 인스턴스에 대한 두개의 강한 참조가 발생한다. 따라서, 이제 하나의 Person
인스턴스에 대한 3개의 강한 참조가 존재한다.
reference2 = reference1
reference3 = reference1
만약 nil
을 할당함으로써 강한 참조들 중 2개를 해제시키면, 하나의 강한 참조만 남게 되고. Person
인스턴스는 메모리에서 해제되지 않는다.
reference1 = nil
reference2 = nil
마지막 세 번째 변수에까지 nil
을 할당하면 마지막 강한 참조가 깨지게 되어 Person
인스턴스가 메모리에서 해제된다.
Person
클래스의 deinit
이 호출되는 것을 확인할 수 있다.
reference3 = nil
// Prints "John Applessed is being deinitailaized"
Strong Reference Cylces Between Class Instances
위에서 본 것처럼, ARC는 클래스 인스턴스에 대한 참조 횟수를 카운팅하고 더 이상 사용되지 않을 때(RC가 0) 할당 해제 한다. 그러나, 클래스의 인스턴스의 강한 참조 횟수가 절대 0이 될 수 없는 상황이 생길 수 있다. 이는 두 개의 클래스 인스턴스가 서로를 강한 참조할 때 발생할 수 있고, 이를 강한 참조 순환(Strong Reference Cycle)이라고 한다. 강한 참조 순환이 발생하면 인스턴스가 메모리에서 해제되지 않아 메모리 낭비를 발생시킨다.
다음의 코드는 강한 참조 순환 문제가 어떻게 발생되는지 보여주는 예시다. Person
인스턴스는 String
타입의 name
프로퍼티와 초기값이 nil
인 옵셔널 apartement?
프로퍼티를 갖고 있다. apartment
가 옵셔널인 이유는 사람이 항상 아파트를 소유하고 있지는 않기 때문이다. Apartment
인스턴스도 마찬가지로 거의 동일한 구조.
class Person {
let name: String
init(name: String){ self.name = name }
var apartment: Apartment?
deinit { print("\\(name) is being initialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \\(unit) is being deinitialized") }
}
Person
인스턴스와 Apartment
인스턴스를 생성하여 각각 john
, unit4A
변수에 할당해준다. 이로 인해 각각 강한 참조가 발생한다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
이제 두 인스턴스를 서로 연결한다. Person
은 Apartment
를 Apartment
는 Person
을 강한 참조하게 된다.
john!.apartment = unit4A
apartment!.tenant = john
이제 john
, unit4A
변수에 각각 nil
을 할당해서 두 변수의 강한 참조를 제거해보자.
john = nil
unit4A = nil
서로를 강한 참조하고 있기 때문에 john
, unit4A
변수의 강한 참조를 제거해도 RC는 0이 될 수 없고, 인스턴스들은 ARC에 의해 할당 해제되지 않는다. 강한 참조 순환으로 인해 메모리 누수가 발생되는 것이다.
Resolving Strong Reference Cycles
강한 참조 순환 문제를 해결하기 위해 Swift는 두 가지 방법을 제공한다.
바로 약한 참조(weak reference)와 미소유 참조(unowned reference).
Weak Reference
약한 참조는 인스턴스를 강하게 잡고있지 않기 때문에 약한 참조가 남아있더라도 인스턴스가 메모리에서 해제될 수 있다. 이때, ARC는 약한 참조에 nil
을 할당한다. 약한 참조는 런타임 시에 nil
값으로 변화될 수 있기 때문에 항상 옵셔널 타입의 변수로 선언해야 한다.
다음의 코드는 위에서의 코드와 거의 동일하지만, 한 가지 중요한 차이는 Apartment
의 tenant
프로퍼티가 약한 참조로 선언되어 있다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\\(name) is being initialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \\(unit) is being deinitialized") }
}
위에서 그랬던 것과 마찬가지로 두 변수의 강한 참조와 두 인스턴스의 연결을 생성하면
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unt4A!.aprtment = john
Person
인스턴스는 여전히 Apartment
를 강한 참조하고 있지만 Apartment
는 Person
을 약한 참조하고 있다. (RC가 증가되지 않는다.)
이는 john
변수에 nil
을 할당해서 강한 참조를 깼을 때 더 이상 Person
인스턴스를 향한 강한 참조가 발생하지 않는다는 의미이다.
john = nil
// Prints "John Appleseed is being deinitialized"
이제 Person
인스턴스를 향한 강한 참조가 없기 때문에 Person
인스턴스는 메모리에서 해제되고, tenant
프로퍼티에 nil이 할당된다.
마지막으로 unit4A
에 nil
을 할당에서 마지막 강한 참조를 깨버리면 Apartment
에 대한 강한 참조가 없어지므로 Apartment
인스턴스 역시 메모리에서 해제된다.
Unowned Reference
약한 참조와 마찬가지로 미소유 참조는 참조하는 인스턴스를 강하게 잡지 않는다. 약한 참조와 다른 점은 미소유 참조는 다른 인스턴스의 life time이 같거나 더 긴 경우에 사용된다는 것이다. 약한 참조를 위해선 unowned
키워드를 프로퍼티나 변수 앞에 작성하면 된다.
미소유 참조는 언제나 값을 갖는다고 예측되기 때문에 ARC는 미소유 참조의 값을 절대 nil
로 설정하지 않는다. 이는 미소유 참조는 옵셔널 타입 이어선 안된다는 뜻이다.
미소유 참조는 메모리에서 해제되지 않는 인스턴스를 참조하는 것이 확실할 때만 사용해라. 만약 참조하는 인스턴스가 할당 해제된 후에 미소유 참조의 값에 접근한다면 런타임 에러가 발생할 것이다.
이 예시는 Customer
, CreditCard
클래스를 정의한다. 두 클래스는 서로의 인스턴스를 프로퍼티로 저장하고 이 관계는 강한 참조 순환을 유발할 수 있다. 두 클래스의 관계는 위에서 살펴본 Person
과 Apartment
의 관계와는 조금 다르다. 이 데이터 모델에서 고객은 신용카드를 가질 수도 있고 안 가질수도 있다. 그러나 신용카드는 언제나 고객이 있어야 존재할 수 있다. CreditCard
인스턴스는 참조하는 Customer
보다 오래 존재할 수 없다. 따라서 Customer
클래스는 옵셔널 타입의 card
프로퍼티를 소유하고, CreditCard
클래스는 unowned(non-optional) customer
프로퍼티를 소유한다.
아래에서 볼 수 있듯 CreditCard
인스턴스는 number
값과 customer
인스턴스가 존재해야 생성될 수 있다(CreditCard
클래스의 이니셜라이저를 확인해라). 신용 카드는 그것을 사용하는 고객이 있어야 존재할 수 있기 때문에 customer
프로퍼티를 미소유 참조하고, 이로 인해 강한 참조 순환을 피할 수 있다.
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") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
이제 Customer
인스턴스는 CreditCard
인스턴스를 강한 참조하고, CreditCard
인스턴스는 Customer
인스턴스를 미소유 참조한다.
미소유 참조로 인하여 변수 john
의 강한 참조가 깨졌을 때 Customer
을 향한 강한 참조가 존재하지 않는다.
Customer
인스턴스를 향한 강한 참조가 존재하지 않기 때문에, Customer
인스턴스는 메모리에서 해제된다. 메모리에서 해제되면 CreditCard
인스턴스에 대한 강한 참조 역시 사라지기 때문에 CreditCard
인스턴스도 메모리에서 해제될 수 있다.
이 처럼 변수 john
의 값에 nil
을 할당하면 Customer
인스턴스와 CreditCard
인스턴스의 deinitialized 메시지가 출력된다.
john = nil
// Prints "John Appleseed is being deinitailaized"
// Prints "Card #1234567890123456 is being deinitailized"
Unowned Optional References
미소유 참조를 옵셔널 타입으로 사용할 수도 있다. 미소유 옵셔널 참조와 약한 참조는 같은 맥락에서 사용될 수 있는데, 차이점은 미소유 옵셔널 참조를 사용할 때는 항상 유효한 객체를 참조하거나 nil
로 설정되어야 함을 보장해야 한다는 것이다.
약한 참조도 미소유 옵셔널 참조와 다르지 않는데 왜 굳이 미소유 옵셔널 참조를 만들었을까? 개인적인 생각은..
약한 참조, 미소유 옵셔널 참조 모두 nil
이 될 수 있다는 특징이 있지만, weak로 선언된 변수는 기본적으로 nil
이 할당되는 경우가 참조하는 인스턴스가 메모리에서 해제되는 경우이다. 즉, 나중에 nil
할당해야 하니까 옵셔널로 선언해야하는 것이고, 미소유 옵셔널로 선언된 변수는 변수의 값이 유요한 객체를 참조하거나, nil
이어야 한다. 이는 ARC 관점에서가 아닌 코드상에서 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
는 각각의 과정에 강한 참조를 유지한다. 학과는 과정을 소유하는 것이다. Course
는 학과에 대한 미소유 참조와 학생들이 수강해야하는 다음 과정에 대한 미소유 참조를 가진다. 모든 과정은 학과의 일부로서 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]
위 코드는 다음과 같은 관계를 갖게 된다.
미소유 옵셔널 참조는 클래스 인스턴스를 강하게 참조하지 않으므로 ARC가 해당 인스턴스를 메모리에서 해제할 수 있다. 이는 미소유 옵셔널 참조가 nil
을 가질 수 있다는 점을 제외하면 미소유 참조와 같다.
논 옵셔널 미소유 참조와 같이 nextCourse
는 언제나 자신보다 오래 존재하는 인스턴스를 참조해야 한다. 예를 들어, department.courses
에서 하나의 과정을 지운다면, 지운 과정을 참조하는 다른 과정 역시 지워야 한다.
Unowned References and Implicitly Unwrapped Optional Properties
약한참조, 미소유 참조 통해서 강한 참조 순환 문제를 해결하는 예제를 살펴 보았다. Person
과 Apartment
예제는 강한 참조 순환이 발생할 수 있는 상황에서 두 프로퍼티가 nil
이 될 수 있게 만들어 해결했다. Customer
와 CreditCard
예제는 한 프로퍼티만 nil
이 될 수 있고, 다른 하나는 nil
이 될 수 없도록 해서 강한 참조 순환 문제를 해결했다.
그러나, 두 프로퍼티가 언제나 값을 가지며, 초기화가 완료되면 프로퍼티가 nil
이 될 수 없는 상황도 존재한다. 이 경우, 미소유 프로퍼티와 암시적 언랩핑 프로퍼티를 함께 사용하여 해결할 수 있다. 이를 통해 초기화가 완료되면 두 프로퍼티에 직접 접근(옵셔널 언래핑 없을 안하고) 할 수 있고 동시에 순환 참조 문제도 피할 수 있다. 이번 예제는 이 관계를 어떤 식으로 설정하는지 알아본다.
예제 코드: 서로의 인스턴스를 프로퍼티로 갖는 두 클래스가 선언되어 있다. 이 데이터 모델에서는 국가는 언제나 수도를 가지고, 모든 도시는 언제나 국가에 속한다. 이를 위해 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
의 이니셜라이저는 self
를 City
이니셜라이저에 전달할 수 없다. 이를 처리하기 위해서, capitalCity
프로퍼티를 암시적 언래핑 옵셔널로 선언한 것이다(City!
). 이는 capitalCity
가 다른 옵셔널 처럼 nil
이 디폴트 값이지만, 다른 옵셔널처럼 옵셔널 언래핑을 할 필요 없이 값에 접근할 수 있다는 뜻이다. capitalCity
는 nil
을 디폴트 값으로 가지기 때문에 새 Country
인스턴스는 이니셜라이저에서 name
프로퍼티에 값이 설정되면 완전히 초기화 되었다고 여겨진다. 이는 Country
이니셜라이저가 참조될 수 있고 self
프로퍼티로 전달할 수 있다는 것을 의미한다.
이 관계를 통해서 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"