[Swift] ARC(2) - 강한참조싸이클 & 약한참조(Weak, Unowned)

Annie Kang·2024년 1월 9일
1

Swift

목록 보기
9/9
post-thumbnail

안녕하세요, Annie 입니다:)
이전에 ARC에 대해서 간략하게 알아보았는데 이번엔 ARC에서 너무 중요한 '강한참조싸이클(Strong Reference Cycle)'에 대해서 알아보겠습니다.


강한참조(Strong Reference)

ARC는 Class나 Closure같은 참조타입의 경우 RC(Reference Count)를 통해 메모리 해제 시점을 관리하는 시스템이였습니다.
해당되는 instance를 다른 property가 참조하는 경우 RC가 1씩 증가하고 RC가 0이 될때 메모리에 할당 되어있던 instance는 메모리에서 해지되게 됩니다.

이런식으로 참조하여 RC가 증가하게 되는 경우 참조하는 instance를 strong(강하게) 참조한다고 합니다. 그래서 기본적인 참조형태가 '강한참조' 라고 볼 수 있습니다.

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

예를 들어, 위와같이 Person이라는 class를 선언한 후 init과 deinit시 문구가 print 되도록 하였습니다.

var reference1: Person?
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

reference1이라는 변수 선언 후, Person 인스턴스를 생성해주면 "John Appleseed is being initialized" 라는 문구가 print 됩니다. 이로써 Person의 RC는 1 증가하게 됩니다.

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

reference1에 nill을 할당하게 되면 Person 인스턴스의 RC는 0이 되고 "John Appleseed is being deinitialized" 가 print 되며 Person 인스턴스는 메모리에서 해지됩니다.

강한참조싸이클(Strong Reference Cycle)

두 개 이상의 Class에서 서로를 강하게 참조하는 경우에는 각 인스턴스에 nil을 할당하더라도 서로를 붙잡고 있기 때문에 RC가 0이 되지 않아 메모리에서 해지되지 않습니다.
이를 강한참조싸이클 라고 하는데 이 경우 메모리누수가 발생하게 됩니다.

간단한 예시와 함께 보여드릴게요.

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

위에 Person과 Apartment 라는 Class를 만들어주었습니다. 보시다시피, 두 클래스의 property(apartment, tenant)는 각각의 Class를 참조하고 있습니다.

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

두 class에 대해 instance를 생성해주었습니다. 이 경우 참조는 아래와 같이 일어납니다.

jonh의 apartment와 unit4A의 tenant는 optional 타입으로 생성시 값을 부여하지 않았기에 자연스레 nill값을 가지게 됩니다.

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

그 값을 위와 같이 부여하게 되면, 참조형태는 아래와 같아집니다.

Person과 Apartment 인스턴스는 서로를 참조하게 되고 두 인스턴스 모두 RC는 2로 증가하게 됩니다.

john = nil
unit4A = nil

이와 같은 경우 john과 unit4A에 nil을 할당하여도 apartment와 tenant는 서로 참조하기 때문에 각 instance의 RC는 1이 됩니다.

서로를 참조하여 메모리해제가 일어나지 않고 이를 강한참조싸이클이라고 합니다. 이는 메모리누수를 일으킵니다.

해결법

  • weak reference(약한참조)
  • unowned reference

각 instance가 서로를 참조하는 것을 막기위해선 위의 두 가지 방법이 있습니다. 하나하나 설명해보도록 하겠습니다 !

Weak Reference(약한참조)

약한참조는 참조하는 instance에 약하게 참조하여 RC를 증가시키지 않아 메모리해제가 일어날 수 있도록 해줍니다.

약한참조를 하기 위해서는 property 앞에 weak 이라는 키워드를 붙이면 됩니다.

예를 들어,

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

Person class의 apartment는 그대로 진행하고, Apartment class의 tenant 앞에 weak을 붙였습니다. 이런 경우 tenant는 참조하고 있는 Person에 대해서 약한참조를 하게 되어 인스턴스를 생성시 Person의 RC를 증가시키지 않습니다.

var john: Person?
var unit4A: Apartment?


john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")


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

따라서 위와 같이 구현 시, jonh.apartment는 Apartment 인스턴스의 RC를 1 증가 시키지만(강한참조), unit4!.tenant는 Person 인스턴스의 RC를 증가시키지 않습니다(약한참조).

  • Person의 RC: 1
  • Apartment의 RC: 2
john = nil
// Prints "John Appleseed is being deinitialized"

위와 같이 john에 nill을 부여하면, RC가 1이 였던 Person 인스턴스는 RC가 0이 되며 메모리에서 해지됩니다.

Person 인스턴스가 해지되었기때문에, Apartment 인스턴스의 RC는 1이 되며 더이상 서로를 참조하지 않게 되며 해지된 Person를 참조하고 있던 tenant 요소는 nil이 자동 부여 됩니다. 따라서 약한참조를 하는 property 는 무조건 optional 값이여야 합니다.

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

남은 unit4A에 nill을 할당하게 되면 Apartment 인스턴스 또한 메모리해지가 일어납니다.

Unowned Reference

약한참조와 동일하게 unowned reference 또한 참조하는 인스턴스의 RC를 증가시키지 않으며 약한참조에서 property 앞에 weak을 붙인 것과 다르게 unowned 를 붙이면 됩니다.

약한 참조와 비슷하게 작동하지만 다른 점이 있는데요,

메모리 해제된 인스턴스를 참조하는 경우, 약한참조는 해당 값에 자동으로 nil을 제공하지만 unowned reference의 경우 nil을 제공하지 않습니다(옵셔널사용 불가).

따라서, unowned reference의 경우 참조하는 인스턴스가 더 긴 lifetime을 가지는 경우에만(무조건 존재하는 상태여야함) 사용이 가능합니다.

이 또한 예를 들어 설명해보겠습니다.

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

Customer 클래스의 변수 card는 CreditCard 를 참조하고, CreditCard 클래서의 상수 customer는 Customer를 참조합니다.

변수 card는 강한참조, 상수 customer는 unowned를 붙임으로서 unowned reference를 하고 있습니다.

var john: Customer?

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

위와 같이 john에 Customer 인스턴스 값을 넣고, john.card 값은 CreditCard 인스턴스로 설정하였습니다.

위와 같이 각 인스턴스를 참조하게 되고 RC값은 아래와 같이 됩니다.

  • Customer의 RC: 1
  • CreditCard의 RC: 1

이때, jonh 값에 nil을 값을 주면 RC가 1이였던 Customer의 RC는 0이 되며 메모리해지가 일어나고, 그로인해 CreditCard의 RC도 0이 되고 메모리 해지가 됩니다.

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

Class에서 일어나는 강한참조싸이클에 대해 알아보았습니다. 다음 글에선 Closure에서 일어나는 강한참조싸이클에 대해서 예시와 함께 알아볼게요 !


참고문헌

profile
성장하는 iOS 개발자

0개의 댓글

관련 채용 정보