4 minute read

❓strong, weak ,unowned

ARC 전편에서는 ARC가 무엇이고 다른 메모리 관리 기법과 비교해보며 이해를 해보았습니다. 이번에는 ARC에서 자주 언급되는 내용인 strong, weak, unowned을 알아보며 메모리 누수를 예방해봅시다

강한 순환 참조 ??

앞선 예제에서 보았다시피 ARC에서 기본적으로 참조 횟수에 대해 추적하고 있기 때문에 더이상 사용하지 인스턴스는 자동으로 메모리에서 해제되게 됩니다. 하지만 절대로 메모리에서 해제되지 않는 경우도 있습니다. 바로 강한순환참조 때문에 말이죠

class Person {
	let name: String
	init(name: String) { self.name = name }
  var apartment: Apartment?
	deinit { print("deinitalized")}
}

class Apartment {
	let unit: String
	init(unit: String) { self.unit = unit }
	var tenant: Person?
	deinit { print("deinitalized")}
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")//john은 Person에 대한 강한 참조를 갖음
unit4A = Apratment(unit: "4A")//unit4A는 Apartment에 대한 강한 참조를 갖음

이 코드의 흐름을 보면 var john 에 대해선 Person 인스턴스로 name과 apartment가 있고 var unit4A 에 대해선 Apartment 인스턴스로 unit과 tenant가 있습니다. 여기서 강한 참조를 갖는 이유는 기본적으로 weak, unowned로 설정해주지 않는 경우에 strong으로 사용되기 때문입니다.

john!.apratment = unit4A
unit4A!.tenant = john

인스턴스 안의 apartment와 tenant가 각각 Apartment, Person 인스턴스를 참조하고 있는 상황이 됩니다. 이로써 Person 인스턴스의 참조 횟수는 2이고 Apartment 참조 횟수도 2가 된 상황입니다.

john = nil
unit4A = nil
//이렇게 해도 참조카운트가 0이 되지 않아서 deinit은 호출되지 않음

인스턴스 해지를 해보면 두 인스턴스는 해지되지 않습니다. 왜냐하면 Person 인스턴스와 Apartment 인스턴스의 변수가 각각 상호 참조를 하고 있어 참조 횟수가 1이기 때문에 이 두 인스턴스는 해지되지않고 메모리 누수 가 발생합니다

이 상황에서 해결할 수 있는 방법은 weakunowned 를 통해 가능합니다.

강한 참조 순환 문제 해결

weak(약한 참조)

해당 인스턴스에 참조 횟수를 증가시키지 않으며 참조하고 있는 인스턴스가 먼저 메모리에서 해제될 때 사용합니다.

약한 참조로 선언하게 되면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에 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

아까 강한 순환 참조를 일으킨 코드와 별 다른 차이점을 느끼지 못할 수도 있습니다. 이 코드에서 가장 중요한 부분은 tenant 를 어떻게 선언해줬나 입니다. 약한 참조로 선언해줬으니 참조 횟수에도 차이가 생깁니다. Apartment의 tenant변수가 Person 인스턴스를 약한 참조(weak)로 참조하고 있다는 것입니다. 그래서 이 시점에서 Apartment 인스턴스의 참조횟수는 변함없이 2이고 Person 인스턴스에 대한 참조 회수는 변수 john이 참조하고 있는 1회 뿐입니다.

weak 인 상태로 선언된 참조 대상이 해지 되면 런타임에 자동으로 참조하고 있는 변수에 nil 할당하게 됩니다. 즉 해당 객체가 nil이여야 합니다.

john = nil 

서로 강한 순환 참조가 일어나지 않기 때문에 인스턴스 해지가 되면서 더 이상 Person 인스턴스를 참조하는 것이 없어집니다.

unit4A = nil

john이 해지된 이후에 Apartment 인스턴스를 참조하는 개체도 사라지게 되면서 Apartment 인스턴스도 메모리에서 해지됩니다.

unowned(미소유 참조)

해당 인스턴스에 참조 횟수를 증가 시키지 않으며 weak 과 다르게 절대 nil을 할당하지 않습니다. 그리고 인스턴스가 현재 참조하고 있는 것과 같은 생애주기를 갖거나 더 긴 생애주기를 갖기 때문에 그 값이 있다고 판단합니다. unowned는 non-optional 타입이기 때문에 값이 있다고 확신할 때만 쓰는 것이 좋습니다. 값이 없을 때 접근한다면 런타임 에러라는 결과를 가져올 수 있기 때문입니다.

class Customer {
	let name: String
	var card: CreditCard?
	init(name: String) {
		self.name = name
	}
	deinit { print("deinitialized") }
}

class CreditCard {
  let number: UInt64
  unowned let cusomter: Cusomter
  init(number: UInt64, customer: Customer) {
  	self.number= number
    self.customer = customer
  }
  deinit{ print("deinitialized") }
}

var john: Customer?
john = Customer(name: "John")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

해당 코드는 Customer, CreditCard 두 개의 클래슥 존재합니다. 여기서 unowned이 사용된 곳을 찾을 수 있나요? 바로 CreditCard에 customer입니다. 이유는 고객과 신용카드를 비교해 봤을때 신용카드는 없더라도 사용자는 남아있을 것이기 때문입니다.

john 은 CreditCard 인스턴스, Customer 인스턴스를 하지만 미소유 참조를 하고 있기 때문에 Customer 인스턴스에 대한 참조 횟수는 1이 되게 됩니다.

🥊 weak vs unowned

약한 참조과 미소유 참조는 nil의 유무에 따라 구분지을 수 있습니다. 하지만 이 경우를 제외한 제 3의 경우도 생길 수 있습니다. 이 경우에 바로 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 언래핑을 사용해 참조 문제를 해결합니다

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: Stirng, country: Country) {
    self.name = name
    self.country = country
  }
}

City에 대한 생성자는 Country 생성자 내에서 호출하게 됩니다. Country의 capitalCity는 초기화 단계에서 City 클래스에 초기화 된 후 사용되게 됩니다. 즉 실제로 Country의 capitalCity는 옵셔널이 되어야합니다. 하지만 여기서는 느낌표 연산자(!)를 이용해 명시적으로 강제 언래핑을 시켰습니다. 이 프로퍼티는 nil 값을 기본적으로 가지고 있지만 옵셔널을 벗길 필요없이 접근될 수 있음을 의미합니다. 그렇게 암시적 언래핑이 돼서 Country에서 name을 초기화하는 시점에 self를 사용할 수 있게됩니다.

= City에서는 강한 참조 순환을 피하기 위해 미소유 참조로 country를 선언해서 두 인스턴스를 문제없이 사용할 수 있습니다.