본문 바로가기
공부/[iOS&Swift]

[Swift] ARC(Automatic Reference Counting) - 1

by 인생은아름다워 2022. 2. 5.

 

✏️ ARC(Automatic Reference Counting)

참조 타입의 인스턴스는, 더 이상 참조되지 않을 때 적절히 메모리에서 해제해줘야 한다. 그렇지 않으면, 쓸모없는 메모리 공간을 낭비하게 되고 결국 앱의 성능 저하를 유발할 수 있기 때문이다. Swift는 ARC라는 개념을 통해 이 과정을 자동으로 해 준다. 물론, 이름과 같이 Reference counting이기 때문에 참조 타입의 인스턴스에 대해서만 ARC가 동작하며, 값 타입의 인스턴스(struct, enum의 인스턴스)는 관리해주지 않는다.

다만, 몇몇 경우에 ARC는 메모리를 관리하기 위해 코드 내에서 더 많은 정보를 요구한다. 이번에는 그런 내용 들에 대해서 공부해 보고 ARC로 하여금 애플리케이션 내의 모든 메모리를 잘 관리하도록 하는 법에 대해서 공부한다.

✏️ ARC가 인스턴스를 관리하는 방식

ARC는 이름과 같이 인스턴스 참조의 개수를 카운팅 하여 인스턴스를 메모리에서 해제할지 말지를 결정한다. ARC는 강한 참조의 개수를 counting 하여 최소 하나의 강한 참조가 있어야만 인스턴스를 해제하지 않는다. 예를 들어보자.

class Person {
	var name: String

	init(name: String) {
		self.name = name
		print("생성 완료!")
	}
	
	deinit {
		print("해제 완료!")
	}
}

var reference1: Person?
var reference1: Person?

reference1 = Person("JM") // 강한 참조 + 1
// 생성 완료!
reference2 = reference1   // 강한 참조 + 1
reference1 = nil   // 강한 참조 - 1
reference2 = nil   // 강한 참조 - 1 -> 전체 강한 참조 개수 = 0
// 해제 완료!

Yagom님의 Swift책 에서는, 위와 같이 강한 참조를 유지하는 것을 ARC에게 인스턴스를 유지할 명분을 주는 것이라고 설명한다.(그렇지 않으면 ARC는 인스턴스를 메모리에서 해제할 테니까!)

참조의 기본은 강한 참조이기 때문에 우리가 매일 써오던 var, let 등은 자연스레 강한 참조로 선언한 것이었다!

✏️ Strong Reference Cycles Between Class Instances(강한 참조 순환 문제)

강한 참조가 순환으로 생겼을 때 문제가 생길 수 있다. 다음의 경우를 보자.

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

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed") // Person instance 강한참조 + 1
unit4A = Apartment(unit: "4A") // Apartment instance 강한참조 + 1

john!.apartment = unit4A // Apartment instance 강한참조 + 1
unit4A!.tenant = john // Person instance 강한참조 + 1

john = nil // Person instance 강한참조 - 1 -> Person instance 참조 카운트 : 1
unit4A = nil // Apartment instance 강한참조 - 1 -> Apartment instance 참조 카운트 : 1

코드를 쭉 읽어보면, 전혀 잘 못한 것이 없어 보이는데 이 코드의 끝에는 메모리의 누수가 발생하고 있다.

Person의 instance와, Apartment의 instance 중 어느 것에도 접근할 수가 없는데, 둘 중 어떤 것의 deinitializer도 호출되지 않은 것으로 명확하게 알 수 있다.

그림으로 보면 이렇다. john과 unit4A라는 변수를 생성하고, 각각 Person의 인스턴스, Apartment의 인스턴스를 할당해주어 강한 참조를 올려놓았고 이 이후에 내부에서 서로 인스턴스를 한 번씩 더 참조한다.

그리고 마지막으로 john과 unit4A의 인스턴스를 nil로 만들어 주어 이 악재를 완성하는 것이다.

이러한 상황을 강한 참조 순환 문제라 하고, 메모리의 누수를 발생시키는 대표적인 요인이다.

이런 상황을 이해했으면, 어떻게 문제를 해결하는지도 알아봐야겠다.

대표적인 두 방법이 있다. 하나는 약한 참조(Weak reference)를 통한 해결, 다른 하나는 미소유 참조(Unowned reference)를 통한 해결이다.

공식문서에서는 다음과 같이 어떤 방법을 쓰는 것이 적절한지에 대해 설명한다.

다른 인스턴스가 더 짧은 생명주기를 가질 때 약한 참조, 반대로 다른 인스턴스가 같거나 더 긴 생명주기를 가질 때 미소유 참조를 사용하는 것이 적절하다!

예를 들어 위의 예제에서는, 아파트가 생명주기 동안에 거주자를 갖지 않을 수 있는 것이 적절하기에, 이럴 때는 약한 참조를 통해 순환 문제를 해결하는 것이 적절하다고 한다.

✏️ Weak Reference(약한 참조)

약한 참조(Weak reference)는 강한 참조와는 다르게 참조하는 인스턴스의 강한 참조 횟수를 증가시키지 않는다. 그렇기 weak 키워드를 변수 선언 앞에 써주면 약한 참조를 할 수 있다. 중요한 것은 약한 참조를 사용했을 경우, 참조하는 인스턴스의 강한 참조 카운트가 0이 되는 경우 인스턴스가 메모리에서 해제될 수 있다는 것이다. 이 경우 약한 참조를 사용하는 변수에는 nil이 할당된다.

  • 런타임에 nil이 할당된다. ⇒ 상수를 사용할 수 없고, 변수를 사용해야 한다. 또한 nil이 할당되고자 한다면 optional변수여야만 한다!
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
// "John Appleseed is being deinitialized"
unit4A = nil
// "Apartment 4A is being deinitialized"

코드를 통해 이해해보자.

위의 예시와 모두 똑같은데, 다만 Apartment 클래스의 tenant 프로퍼티만 약한 참조로 Person의 인스턴스를 참조하게 되어있다.

Person의 인스턴스의 강한 참조 카운트 : 1

Apartment의 인스턴스의 강한참조 카운트 : 2

이 경우 john이라는 객체를 메모리에서 해제한다면, 아래와 같이 Person 인스턴스의 강한참조 카운트는 0으로 된다. (var john → Person instance 가 사라지면서) → 메모리에서 해제되게 되며 tenant프로퍼티의 값은 nil이 할당되게 되는 것이다.

unit4A = nil 이 코드 통해 두 인스턴스를 메모리에서 안정적으로 해제할 수 있게 된다.

이렇게 강한 참조 순환 문제를 해결할 수 있다. 다만 약한 참조에서 주의할 것은 언제나 참조하고 있는 인스턴스가 메모리에서 해제되어 nil이 할당될 수 있다는 것을 인지하고 있어야 한다는 것이다!

✏️ Unowned Reference(미소유 참조)

Weak reference 말고도 강한 참조 count를 증가시키지 않을 수 있는데, 다른 방법은 미소유 참조(Unowned reference)를 이용하는 것이다. 둘 다 강한 참조 카운트를 증가시키지 않는 공통점이 있지만 미소유 참조의 경우는 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 동작한다.

그러니까, 참조하는 인스턴스가 메모리에서 해제된다 하더라도 자동으로 nil을 할당해주지 않는다는 것이다. (Weak reference에서는 참조하는 인스턴스가 메모리에서 해제되면 자동으로 nil을 할당했었다.

미소유 참조는 인스턴스 선언 앞에 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") }
}

var john: Customer?

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

john = nil
// "John Appleseed is being deinitialized"
// "Card #1234567890123456 is being deinitialized"

공식문서에서는 신용카드와 사용자의 예시를 든다.

신용카드는 항상 명의자를 가진다고 생각하기에 unowned reference로 john을 할당!

이 상황까지는 Customer instance의 강한 참조 카운트는 1, CreditCard instance의 강한 참조 카운트는 1이다.

(미소유 참조를 했기 때문에 Customer instance의 강한 참조 카운트가 올라가지 않았다.)

여기서 변수 john의 강한 참조를 끊는다면?

john = nil에 의해서 Customer instance의 강한 참조 카운트 = 0으로 변경되면서 ARC는 이 인스턴스를 메모리에서 해제시킬 것이다. → deinitializer call

이렇게 되는 순간, CreditCard instance로의 강한 참조 또한 사라질 것이기 때문에 CreditCard instance의 강한 참조 카운트 = 0 → 이 또한 ARC가 메모리에서 해제 → deinitializer call

이를 통해 사람(john)이 사라지면....... 그의 신용카드 또한 함께 사라져야 하는 상황을 구현한 것이다.

🍎  결론

  • Apple은 ARC를 통해 메모리를 관리하는데, Strong reference count라는 값을 증가시켜가며 이 count가 0 일 때 메모리에서 해제하는 방식이다.

→ (Strong reference cycle) 강한 참조 순환 문제 발생 가능

  • Strong reference cycle(강한 참조 순환) 문제를 해결하기 위해 Weak reference(약한 참조)와 Unowned reference(미소유 참조) 두 방법을 활용할 수 있다.
  • Weak reference와 Unowned reference의 차이는 참조하는 인스턴스가 사라졌을 때 nil을 할당하느냐 그렇지 않느냐이다.
  • 참조하고자 하는 인스턴스가 먼저 사라질 수 있는 경우 약한 참조, 참조하고자 하는 인스턴스가 동일한 생명주기 또는 더 길게 살아있을 경우 미소유 참조를 이용하자!

🍎  참고자료

공식문서/ARC

댓글