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")

referenceCycle01_2x

이제 두 인스턴스를 서로 연결한다. PersonApartmentApartmentPerson을 강한 참조하게 된다.

john!.apartment = unit4A
apartment!.tenant = john

referenceCycle02_2x

이제 john, unit4A 변수에 각각 nil을 할당해서 두 변수의 강한 참조를 제거해보자.

john = nil
unit4A = nil

referenceCycle03_2x

서로를 강한 참조하고 있기 때문에 john, unit4A 변수의 강한 참조를 제거해도 RC는 0이 될 수 없고, 인스턴스들은 ARC에 의해 할당 해제되지 않는다. 강한 참조 순환으로 인해 메모리 누수가 발생되는 것이다.

Resolving Strong Reference Cycles

강한 참조 순환 문제를 해결하기 위해 Swift는 두 가지 방법을 제공한다.

바로 약한 참조(weak reference)와 미소유 참조(unowned reference).

Weak Reference

약한 참조는 인스턴스를 강하게 잡고있지 않기 때문에 약한 참조가 남아있더라도 인스턴스가 메모리에서 해제될 수 있다. 이때, ARC는 약한 참조에 nil을 할당한다. 약한 참조는 런타임 시에 nil 값으로 변화될 수 있기 때문에 항상 옵셔널 타입의 변수로 선언해야 한다.

다음의 코드는 위에서의 코드와 거의 동일하지만, 한 가지 중요한 차이는 Apartmenttenant 프로퍼티가 약한 참조로 선언되어 있다.

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를 강한 참조하고 있지만 ApartmentPerson을 약한 참조하고 있다. (RC가 증가되지 않는다.)

weakReference01_2x

이는 john 변수에 nil을 할당해서 강한 참조를 깼을 때 더 이상 Person 인스턴스를 향한 강한 참조가 발생하지 않는다는 의미이다.

john = nil
// Prints "John Appleseed is being deinitialized"

이제 Person 인스턴스를 향한 강한 참조가 없기 때문에 Person 인스턴스는 메모리에서 해제되고, tenant 프로퍼티에 nil이 할당된다.

weakReference02_2x

마지막으로 unit4Anil을 할당에서 마지막 강한 참조를 깨버리면 Apartment에 대한 강한 참조가 없어지므로 Apartment 인스턴스 역시 메모리에서 해제된다.

weakReference03_2x

Unowned Reference

약한 참조와 마찬가지로 미소유 참조는 참조하는 인스턴스를 강하게 잡지 않는다. 약한 참조와 다른 점은 미소유 참조는 다른 인스턴스의 life time이 같거나 더 긴 경우에 사용된다는 것이다. 약한 참조를 위해선 unowned 키워드를 프로퍼티나 변수 앞에 작성하면 된다.

미소유 참조는 언제나 값을 갖는다고 예측되기 때문에 ARC는 미소유 참조의 값을 절대 nil로 설정하지 않는다. 이는 미소유 참조는 옵셔널 타입 이어선 안된다는 뜻이다.

미소유 참조는 메모리에서 해제되지 않는 인스턴스를 참조하는 것이 확실할 때만 사용해라. 만약 참조하는 인스턴스가 할당 해제된 후에 미소유 참조의 값에 접근한다면 런타임 에러가 발생할 것이다.

이 예시는 Customer, CreditCard 클래스를 정의한다. 두 클래스는 서로의 인스턴스를 프로퍼티로 저장하고 이 관계는 강한 참조 순환을 유발할 수 있다. 두 클래스의 관계는 위에서 살펴본 PersonApartment의 관계와는 조금 다르다. 이 데이터 모델에서 고객은 신용카드를 가질 수도 있고 안 가질수도 있다. 그러나 신용카드는 언제나 고객이 있어야 존재할 수 있다. 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 인스턴스를 미소유 참조한다.

unownedReference01_2x

미소유 참조로 인하여 변수 john의 강한 참조가 깨졌을 때 Customer을 향한 강한 참조가 존재하지 않는다.

unownedReference02_2x

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]

위 코드는 다음과 같은 관계를 갖게 된다.

unownedOptionalReference_2x

미소유 옵셔널 참조는 클래스 인스턴스를 강하게 참조하지 않으므로 ARC가 해당 인스턴스를 메모리에서 해제할 수 있다. 이는 미소유 옵셔널 참조가 nil을 가질 수 있다는 점을 제외하면 미소유 참조와 같다.

논 옵셔널 미소유 참조와 같이 nextCourse는 언제나 자신보다 오래 존재하는 인스턴스를 참조해야 한다. 예를 들어, department.courses 에서 하나의 과정을 지운다면, 지운 과정을 참조하는 다른 과정 역시 지워야 한다.

Unowned References and Implicitly Unwrapped Optional Properties


약한참조, 미소유 참조 통해서 강한 참조 순환 문제를 해결하는 예제를 살펴 보았다. PersonApartment 예제는 강한 참조 순환이 발생할 수 있는 상황에서 두 프로퍼티가 nil이 될 수 있게 만들어 해결했다. CustomerCreditCard 예제는 한 프로퍼티만 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의 이니셜라이저는 selfCity 이니셜라이저에 전달할 수 없다. 이를 처리하기 위해서, capitalCity 프로퍼티를 암시적 언래핑 옵셔널로 선언한 것이다(City!). 이는 capitalCity가 다른 옵셔널 처럼 nil이 디폴트 값이지만, 다른 옵셔널처럼 옵셔널 언래핑을 할 필요 없이 값에 접근할 수 있다는 뜻이다. capitalCitynil을 디폴트 값으로 가지기 때문에 새 Country 인스턴스는 이니셜라이저에서 name 프로퍼티에 값이 설정되면 완전히 초기화 되었다고 여겨진다. 이는 Country 이니셜라이저가 참조될 수 있고 self 프로퍼티로 전달할 수 있다는 것을 의미한다.

이 관계를 통해서 CountryCity 인스턴스를 강한 참조 순환 없이 한 줄의 코드로 생성할 수 있고 뿐만 아니라, 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"

Tags:

Categories:

Updated: