🔥 클래스 인스턴스 사이의 강한 참조 순환 해결하기
강의 목차
Swift는 클래스 타입의 속성으로 작업할 때 강한 참조 순환을 해결하기 위해 두 가지 방법을 제공합니다: 약한 참조(weak reference)와 미소유 참조(unowned reference)입니다.
약한 참조와 미소유 참조는 참조 순환 내의 한 인스턴스가 다른 인스턴스를 강하게 잡지 않고 참조할 수 있도록 해줍니다. 그러면 인스턴스들이 강한 참조 순환을 만들지 않고도 서로를 참조할 수 있게 됩니다.
다른 인스턴스의 수명이 더 짧을 때, 즉 다른 인스턴스가 먼저 할당 해제될 수 있을 때는 약한 참조를 사용하세요. Apartment
예제에서, 아파트는 수명 주기 동안 어떤 시점에는 세입자가 없을 수도 있으므로 이 경우 약한 참조가 참조 순환을 끊기에 적절한 방법입니다. 반대로 다른 인스턴스의 수명이 같거나 더 길 때는 미소유 참조를 사용하세요.
약한 참조(weak reference)
약한 참조
는 참조하는 인스턴스를 강하게 잡지 않는 참조이므로 ARC가 참조된 인스턴스를 처분하는 것을 막지 않습니다. 이런 동작 덕분에 참조가 강한 참조 순환의 일부가 되는 것을 방지할 수 있습니다. 속성이나 변수 선언 앞에 weak
키워드를 붙여서 약한 참조를 나타낼 수 있습니다.
약한 참조는 참조하는 인스턴스를 강하게 잡지 않기 때문에, 약한 참조가 여전히 인스턴스를 참조하고 있는 동안 그 인스턴스가 할당 해제될 가능성이 있습니다. 그러므로 ARC는 참조하는 인스턴스가 할당 해제되면 자동으로 약한 참조를 nil
로 설정합니다. 그리고 약한 참조는 런타임에 nil
로 값이 변경될 수 있어야 하기 때문에, 항상 옵셔널 타입의 변수로 선언되며 상수로는 선언되지 않습니다.
다른 옵셔널 값과 마찬가지로 약한 참조에 값이 존재하는지 확인할 수 있으며, 더 이상 존재하지 않는 유효하지 않은 인스턴스를 참조하게 되는 일은 절대 없을 것입니다.
아래 예제는 위의 Person
과 Apartment
예제와 동일하지만, 한 가지 중요한 차이점이 있습니다. 이번에는 Apartment
타입의 tenant
속성이 약한 참조로 선언됩니다:
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") } }
swift
두 변수(john
과 unit4A
)로부터의 강한 참조와 두 인스턴스 사이의 연결은 이전과 같이 만들어집니다:
var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A") john!.apartment = unit4A unit4A!.tenant = john
swift
두 인스턴스를 서로 연결한 후의 참조는 다음과 같습니다:
Person
인스턴스는 여전히 Apartment
인스턴스에 대한 강한 참조를 가지고 있지만, Apartment
인스턴스는 이제 Person
인스턴스에 대한 약한
참조를 가지고 있습니다. 이것은 john
변수가 가진 강한 참조를 끊어 nil
로 설정하면, Person
인스턴스에 대한 강한 참조가 더 이상 없다는 것을 의미합니다:
john = nil // Prints "John Appleseed is being deinitialized"
swift
Person
인스턴스에 대한 강한 참조가 더 이상 없기 때문에, 할당이 해제되고 tenant
속성은 nil
로 설정됩니다:
Apartment
인스턴스에 대한 유일한 남은 강한 참조는 unit4A
변수로부터 옵니다. 만약 그
강한 참조를 끊으면, Apartment
인스턴스에 대한 강한 참조는 더 이상 없습니다:
unit4A = nil // Prints "Apartment 4A is being deinitialized"
swift
Apartment
인스턴스에 대한 강한 참조가 더 이상 없기 때문에, 그것도 할당 해제됩니다:
미소유 참조(unowned reference)
약한 참조와 마찬가지로 미소유 참조
도 참조하는 인스턴스를 강하게 잡지 않습니다. 그러나 약한 참조와 달리, 미소유 참조는 다른 인스턴스의 수명이 같거나 더 길 때 사용됩니다. 속성이나 변수 선언 앞에 unowned
키워드를 붙여서 미소유 참조를 나타낼 수 있습니다.
약한 참조와 달리, 미소유 참조는 항상 값을 가질 것으로 예상됩니다. 결과적으로 값을 미소유로 표시하는 것은 그 값을 옵셔널로 만들지 않으며, ARC는 절대로 미소유 참조의 값을 nil
로 설정하지 않습니다.
다음 예제는 은행 고객과 그 고객의 가능한 신용카드를 모델링하는 Customer
와 CreditCard
라는 두 클래스를 정의합니다. 이 두 클래스는 각각 다른 클래스의 인스턴스를 속성으로 저장합니다. 이 관계는 강한 참조 순환을 만들 가능성이 있습니다.
Customer
와 CreditCard
사이의 관계는 위의 약한 참조 예제에서 본 Apartment
와 Person
사이의 관계와 약간 다릅니다. 이 데이터 모델에서 고객은 신용카드를 가지고 있을 수도 있고 없을 수도 있지만, 신용카드는 항상
고객과 연관됩니다. CreditCard
인스턴스는 절대 참조하는 Customer
보다 오래 살아남지 않습니다. 이를 나타내기 위해, Customer
클래스는 옵셔널 card
속성을 가지고 있고, CreditCard
클래스는 미소유(그리고 non-optional)의 customer
속성을 가지고 있습니다.
게다가 새로운 CreditCard
인스턴스는 number
값과 customer
인스턴스를 커스텀 CreditCard
이니셜라이저에 전달해야만 생성될 수 있습니다. 이는 CreditCard
인스턴스가 생성될 때 항상 customer
인스턴스와 연관되도록 보장합니다.
신용카드는 항상 고객을 가질 것이기 때문에, 강한 참조 순환을 피하기 위해 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") } }
swift
다음 코드 스니펫은 특정 고객에 대한 참조를 저장하는 데 사용될 john
이라는 옵셔널 Customer
변수를 정의합니다. 이 변수는 옵셔널이기 때문에 초기값이 nil입니다:
이제 Customer
인스턴스를 생성하고, 그것을 사용하여 새로운 CreditCard
인스턴스를 초기화하고 해당 고객의 card
속성으로 할당할 수 있습니다:
john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
swift
두 인스턴스를 연결한 후의 참조는 다음과 같습니다:
Customer
인스턴스는 이제 CreditCard
인스턴스에 대한 강한 참조를 가지고 있고, CreditCard
인스턴스는 Customer
인스턴스에 대한 미소유 참조를 가지고 있습니다.
미소유 customer
참조 때문에, john
변수가 가진 강한 참조를 끊으면, Customer
인스턴스에 대한 강한 참조는 더 이상 없습니다:
Customer
인스턴스에 대한 강한 참조가 더 이상 없기 때문에, 할당이 해제됩니다. 이 후에는 CreditCard
인스턴스에 대한 강한 참조도 더 이상 없으므로, 그것도 할당 해제됩니다:
john = nil // Prints "John Appleseed is being deinitialized" // Prints "Card #1234567890123456 is being deinitialized"
swift
위의 마지막 코드 스니펫은 john
변수가 nil
로 설정된 후에 Customer
인스턴스와 CreditCard
인스턴스의 디이니셜라이저가 각각 "deinitialized" 메시지를 출력하는 것을 보여줍니다.
미소유 옵셔널 참조
클래스에 대한 옵셔널 참조를 미소유로 표시할 수 있습니다. 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 } }
swift
Department
는 학과에서 제공하는 각 과목에 대해 강한 참조를 유지합니다. ARC 소유권 모델에서, 학과는 과목들을 소유합니다. 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]
swift
위의 코드는 학과와 그 학과의 세 과목을 생성합니다. intro와 intermediate 과목은 둘 다 nextCourse
속성에 저장된 추천 후속 과목을 가지고 있는데, 이는 학생이 이 과목을 완료한 후 수강해야 할 과목에 대한 미소유 옵셔널 참조를 유지합니다.
미소유 옵셔널 참조는 래핑하는 클래스의 인스턴스를 강하게 잡지 않으므로, ARC가 인스턴스를 할당 해제하는 것을 막지 않습니다. 미소유 옵셔널 참조는 nil
이 될 수 있다는 점을 제외하면 ARC 하에서 미소유 참조와 동일하게 동작합니다.
비옵셔널 미소유 참조와 마찬가지로, nextCourse
가 항상 할당 해제되지 않은 과목을 참조하도록 보장하는 것은 사용자의 책임입니다. 예를 들어, 이 경우 department.courses
에서 과목을 삭제할 때, 다른 과목이 그 과목에 대해 가질 수 있는 참조도 제거해야 합니다.
미소유 참조와 암시적 언래핑 옵셔널 속성
위의 약한 참조와 미소유 참조 예제는 강한 참조 순환을 끊어야 하는 더 일반적인 시나리오 두 가지를 다룹니다.
Person
과 Apartment
예제는 둘 다 nil
이 될 수 있는 두 속성이 강한 참조 순환을 일으킬 가능성이 있는 상황을 보여줍니다. 이 시나리오는 약한 참조로 가장 잘 해결됩니다.
Customer
와 CreditCard
예제는 nil
이 될 수 있는 속성 하나와 nil
이 될 수 없는 속성 하나가 강한 참조 순환을 일으킬 가능성이 있는 상황을 보여줍니다. 이 시나리오는 미소유 참조로 가장 잘 해결됩니다.
그러나 세 번째 시나리오가 있는데, 여기서는 초기화가 완료되면 둘 다
속성이 항상 값을 가져야 하고 nil
이 되어서는 안 됩니다. 이 시나리오에서는 한 클래스의 미소유 속성과 다른 클래스의 암시적 언래핑 옵셔널 속성을 결합하는 것이 유용합니다.
이는 초기화가 완료되면 두 속성 모두 직접(옵셔널 언래핑 없이) 액세스될 수 있도록 하면서도 여전히 참조 순환을 방지할 수 있게 해줍니다. 이 섹션에서는 그런 관계를 설정하는 방법을 보여줍니다.
아래 예제는 각각 다른 클래스의 인스턴스를 속성으로 저장하는 Country
와 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 } }
swift
두 클래스 사이의 상호의존성을 설정하기 위해, City
의 이니셜라이저는 Country
인스턴스를 받아서 그 인스턴스를 country
속성에 저장합니다.
City
의 이니셜라이저는 Country
의 이니셜라이저 내에서 호출됩니다. 그러나 Two-Phase Initialization에 설명된 대로, 새로운 Country
인스턴스가 완전히 초기화될 때까지 Country
의 이니셜라이저는 self
를 City
이니셜라이저에 전달할 수 없습니다.
이 요구사항을 처리하기 위해, Country
의 capitalCity
속성을 타입 어노테이션 끝에 느낌표가 붙은 암시적 언래핑 옵셔널 속성으로 선언합니다(City!
). 이는 capitalCity
속성이 다른 옵셔널처럼 nil
을 기본값으로 가지지만, Implicitly Unwrapped Optionals에 설명된 대로 값을 언래핑할 필요 없이 액세스될 수 있다는 것을 의미합니다.
capitalCity
가 nil
의 기본값을 가지기 때문에, Country
인스턴스는 이니셜라이저 내에서 name
속성을 설정하는 즉시 완전히 초기화된 것으로 간주됩니다. 이는 Country
이니셜라이저가 name
속성을 설정하는 즉시 암시적인 self
속성을 참조하고 전달하기 시작할 수 있다는 것을 의미합니다. 따라서 Country
이니셜라이저는 자신의 capitalCity
속성을 설정할 때 City
이니셜라이저에 대한 매개변수 중 하나로 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"
swift
위 예제에서 암시적 언래핑 옵셔널을 사용한 것은 두 단계 클래스 이니셜라이저의 모든 요구사항이 충족된다는 것을 의미합니다. capitalCity
속성은 초기화가 완료되면 비옵셔널 값처럼 사용되고 액세스될 수 있으면서도 여전히 강한 참조 순환을 피할 수 있습니다.
참조 개수 증가 및 감소로 각 참조를 정리해 볼게요.
강한 참조(strong reference)
강한 참조는 참조 카운트를 증가시킵니다. 즉, 어떤 객체에 대한 강한 참조를 생성하면, 해당 객체의 참조 카운트가 1 증가합니다. 이는 ARC가 해당 객체를 메모리에 유지하도록 보장합니다. 객체에 대한 모든 강한 참조가 제거되면, 즉 참조 카운트가 0이 되면, ARC는 해당 객체를 메모리에서 해제합니다.
class Person { let name: String init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } } var reference1: Person? var reference2: Person? var reference3: Person? reference1 = Person(name: "John Appleseed") // Prints "John Appleseed is being initialized" // 참조 카운트: 1 reference2 = reference1 // 참조 카운트: 2 reference3 = reference1 // 참조 카운트: 3 reference1 = nil // 참조 카운트: 2 reference2 = nil // 참조 카운트: 1 reference3 = nil // 참조 카운트: 0 // Prints "John Appleseed is being deinitialized"
swift
약한 참조(weak reference)
약한 참조는 참조 카운트를 증가시키지 않습니다. 즉, 어떤 객체에 대한 약한 참조를 생성해도, 해당 객체의 참조 카운트는 변하지 않습니다. 이는 ARC가 해당 객체를 메모리에 유지하도록 보장하지 않는다는 것을 의미합니다. 객체에 대한 모든 강한 참조가 제거되면, ARC는 해당 객체를 메모리에서 해제하고, 모든 약한 참조는 자동으로 nil
로 설정됩니다.
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") } } var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A") john!.apartment = unit4A unit4A!.tenant = john john = nil // Prints "John Appleseed is being deinitialized" unit4A = nil // Prints "Apartment 4A is being deinitialized"
swift
미소유 참조(unowned reference)
미소유 참조도 약한 참조와 마찬가지로 참조 카운트를 증가시키지 않습니다. 그러나 약한 참조와 달리, 미소유 참조는 절대 nil
이 되지 않을 것으로 예상됩니다. 따라서 미소유 참조는 항상 유효한 인스턴스를 참조해야 합니다. 만약 미소유 참조가 이미 해제된 인스턴스를 참조하려고 하면, 런타임 에러가 발생합니다.
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!) john = nil // Prints "John Appleseed is being deinitialized" // Prints "Card #1234567890123456 is being deinitialized"
swift
이런 식으로, 강한 참조, 약한 참조, 미소유 참조는 ARC와 상호작용하여 메모리 관리를 수행하는 방식에 차이가 있습니다. 각 참조 타입은 우리가 코드에서 객체들 사이의 관계를 모델링하는 방식에 따라 적절히 사용될 수 있습니다.