momodudu.zip

#8 Swift - Automatic Reference Counting 본문

ios/Swift

#8 Swift - Automatic Reference Counting

ally10 2019. 12. 28. 18:03

Swift에서는 기본적으로 app 내의 메모리 관리를 지원한다. java의 GC처럼..

사실 C++ 개발자였고 smart pointer는 특별한 경우외에는 쓰지 않았기 때문에 이와 같은 GC 개념은 오히려 나한테 더 어렵다 -.-;

 

간단하게 요약하자면, Swift에서의 ARC는

- class와 같은 레퍼런스 타입에만 적용된다.

- ARC 자체적으로 instance 내의 각 Property가 얼만큼 referencing되는지 tracking한다.

- 이를 위해 strong reference를 사용한다.

 

1. ARC Action

var reference1: Person? = Person()
var reference2: Person?

위와 같은 코드의 경우, ref1,2 둘다 Person의 optional type이지만 1의 경우, Person을 instance화 시켜주었으므로 initializer가 호출되고, 또 이에 대한 strong reference가 생긴다. ARC자체적으로 이 reference를 사용하는 부분이 있는 한, 이 메모리가 삭제되지 않고 유지될 수 있도록 해준다.

 

반면 밑의 referece2의 경우 default값인  nil로 assign되어있고, 실제 객체가 있는게 아니므로 이에 대한 tracking은 하지 않는다.

 

 

2. Strong reference cycle

예시 코드를 보자.

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

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

class Person은 Apartment type class를 물고 있고, Apartment 역시 Person을 물고 있다.

즉, 둘 다 레퍼런싱을 하고 있는 경우 둘이서 서로 레퍼런스가 없어질때까지 계속 대기만 하게 되고 영원히 메모리에서 deload 되지 않는 현상이 발생한다.

 

c++ shared pointer의 순환참조와 똑같은 현상이라고 보면 될 것 같다. c++ 순환참조 문제를 해결하는것과 아주 유사하게, swift에서도 이 순환참조 문제를 해결하고자 한다면 하나의 relationship을 weak/unowned 레퍼런스로 선언해주면 된다.

참고로, 각 클래스 내의 프로퍼티는 var 앞에 아무런 키워드도 붙지 않았는데, 이는 default가 strong이다.

 

 

- weak reference

두 클래스간에 개념적으로 봤을 때 life cycle이 더 짧은 레퍼런스에 weak reference를 사용할 수 있다.

예를들어, Apartment의 property person은 빈집일수도 있으므로 person에 weak reference를 달 수 있다.

 

- unowned reference

두 클래스간에 개념적으로 life cycle이 같거나 혹은 좀 더 긴 레퍼런스에 weak reference를 사용할 수 있다.

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

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

  위와 같은 예시인데, Customer type의 경우 CreditCard보다 더 긴 life cycle 을 갖고 있으므로, unowned로 선언한다.

단, 여기서 주의할것은 unowned는 상수이자 "옵셔널 타입이 아닌" Customer를 갖고 있다. 즉, credit card class의 customer property는 절대로 nil이 될 수 없다.

이는 default값이 주어지지 않으므로 default value든, initializer든 credit card가 생성될때는 반드시 현존하는 customer instance가 존재해야된다는 것을 의미한다.

 

사실 언뜻 봐서는 Person<->Apartment의 관계와 Customer<->CreditCard의 관계는 서로를 물고있다는점에서 유사해보일수도 있다. 하지만 개념적으로나 코드적으로나 따져보면 둘의 관계는 전혀 다르다. Customer는 CreditCard 없이 존재할 수 있지만 CreditCard는 customer instance 없이 만들어질 수 없다. 즉, CreditCard->Customer로 dependency가 존재한다는 얘기다.

그래서 이 관계로 봤을때 creditCard는 customer보다 lifetime이 더 짧다는것이 코드적으로 보장된다. 이런 경우에 unonwed로 설정한다.

 

- unowned + unwrapped optional

strong reference cycle이 발생할 수 있는 세번째 케이스이다. 이 예제는 City와 Country간의 releationship을 정의하고 있는데, 이 두가지는 모두 nil이 정의될 수 없다. 모든 Country는 City를 반드시 가져야 하고, 모든 City역시 속한 Country가 있어야 한다.

즉 옵셔널 타입이 설계에 맞지 않는다. 그래서 옵셔널 타입이 아닌 일반 Country, City 타입을 물고있어야 하는데 이 경우 strong referencey cycle이 발생할 것이다.

 

즉, 위와 같이 두 클래스 개념 모두 Nil이 허용되지 않는 경우 unowned let과 unwrapped optional value를 조합하여 strong reference cycle을 피한다.

 

class Country{
    let name: String
    var capital: City!
    
    init(name: String, capitalName: String){
        self.name = name
        self.capital = 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에 대한 dependancy가 존재하므로 위에서 본 unwoned 예제와 같이 작성하면 된다.

유의해서 봐야할것은 Country의 생성자이다. 먼저 City는 unwrapped optional로 선언되어 있다. 즉, 이 클래스 내의 city는 절대로 없으면 안된다는것을 의미하고, 또 이걸 보장해야한다.

이걸 개런티하기 위해 Country의 생성자에 약간의 트릭이있다. 생성자 파라미터로 city name을 전달받고, 이 전달받은 city name으로 City를 생성자 내부에서 생성해준다.

Country의 생성자가 City를 생성하는 역할을 맡게 되는 것이다. 즉, Country는 반드시 City를 가지고 있음을 보장하게 된다.

이렇게 하나의 객체가 다른 객체의 생성을 맡는 설계 패턴 이름이 있었던것 같은데.... 기억이 안난다... 아님말고 ..

 

무튼 종합적으로 봤을 때, 클래스간의 레퍼런스를 물려줄 때는 아래와 같이 케이스를 나누어서 arc 설계를 해주어야 한다.

1) 두 가지 인스턴스 모두 nil 이 허용되는 경우(위 예시의 Person<->Apartment) : weak reference

2) 하나는 nil이 허용되고, 다른 하나는 nil이 허용되지 않는 경우(Customer<->CreditCard) : unowned let

3) 둘 다 nil 이 허용되지 않는 경우 : unowned let + unwrapped optioanl property 사용

 

 

 

 

'ios > Swift' 카테고리의 다른 글

#10 Swift - Protocol as Interface  (0) 2020.04.29
#9 Swift - class, struct에 대해서 다시 짚어보기  (0) 2020.04.23
#7 Swift - Protocol(1)  (0) 2019.11.28
#6 Swift - Extension  (0) 2019.11.28
#5 Swift - Optional chaining  (0) 2019.11.27