본문 바로가기

IT/Swift

[swift] ARC(Automatic Reference Counting)

[swift 기초문법] - Automatic Reference Counting, ARC

ARC란

ARC는 자동 레퍼런스 카운팅으로서 프로퍼티, 상수, 변수에 참조가 지정되면  그때 ARC에 들어있는 카운트를 증가시키고 프로퍼티, 상수, 변수가 해제되면 카운트를 감소시킨다. 보유한 카운트가 0이 되면 메모리를 해제시킨다.

Swift는 앱의 메모리 사용을 관리하기 위해서 ARC를 이용해 자동으로 참조 횟수를 관리하여 개발자는 메모리 관리에 신경을 줄일 수 있고 ARC가 사용하지 않는 인스턴스를 메모리에서 해지한다. 최소 하나라도 인스턴스에 대한 참조가 있는 경우 메모리를 해제하지 않는다.

Swift에서는 앱의 메모리 사용을 관리하기 위해 ARC(Automatic Reference Counting)을 사용한다. 자동으로 참조 횟수를 관리하기 때문에 대부분의 경우에 개발자는 메모리 관리에 신경쓸 필요가 없고 ARC가 알아서 더 이상 사용하지 않는 인스턴스를 메모리에서 해지한다.

 

 

 

 

ARC 동작

클래스의 인스턴스가 만들어질 때 ARC는 메모리를 할당해준다.

클래스의 인스턴스가 더 이상 필요하지 않을 때 ARC는 할당된 메모리를 해제한다.

ARC는 아직 사용중인 인스턴스를 해제하지 않도록 주의를 기울인다.

ARC는 인스턴스를 참조하고 있는 프로퍼티, 상수, 변수들을 추적하고, 만약 'strong' 참조가 하나라도 있다면 해제하지 않는다.

 

 

ARC 사용

 

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

    deinit {
        println("\(name) is being deinitialized")
    }
}

 

Person 클래스는 이니셜라이저를 가지며 인스턴스의 name 속성을 설정하고 초기화 진행 중이다고 표시하는 메시지를 출력합니다.

Person 클래스는 디이니셜라이저를 가지며 클래스의 인스턴스가 해제될 때 메시지를 출력합니다.

 

// Person 클래스 타입을 갖는 reference 변수 3개를 선언. 모두 옵셔널 변수이므로 초기값은 nil을 갖고 있습니다.
var reference1: Person?
var reference2: Person?
var reference3: Person?

//reference1 변수에 Person 인스턴스 생성하여 참조하게됩니다.
reference1 = Person(name: "John Appleseed")

//나머지 두 변수를 reference1를 참조하게 합니다.
reference2 = reference1
reference3 = reference1

/*이시점의 인스턴스에 대한 참조 횟수는 3이된다. 그런 후 reference1, reference2 참조 해지합니다. 
그렇게 되면 Person 인스턴스에 대한 참조 횟수는 아직 1이어서 Person 인스턴스는 해지되지 않습니다.*/
reference1 = nil
reference2 = nil

/*
Pesron 인스턴스를 참조하고 있는 나머지 변수 refernce3의 참조 해지하면 
더이상 Person 인스턴스를 참조하고 있는 것이 없으므로 ARC가 Person 인스턴스 메모리를 해지합니다.
*/
reference3 = nil

 

 

 

강한 참조란 (Strong Reference)

swift에서 발생하는 Reference는 기본적으로 strong reference로 처리합니다. 때문에 별도의 식별자를 명시하지 않으면 강한 참조를 합니다.

강한 참조를 사용하면 참조 횟수가 1이 증가됩니다.

강한 참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당해주면 참조 회수가 1 감소합니다.

 

 

강한 참조 순환이란 (Strong Reference Cycle)

reference type끼리 서로를 가리킬 때 발생합니다.

특히 서로 다른 두 class의 property끼리 서로 상대방의 인스턴스를 강하게 참조하는 경우가 대표적인 예입니다.

 

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

      deinit { print("\(name) is being deinitialized") }
}

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

var john: Person? = Person(name: "john appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A

unit4A!.tenant = john //두 인스턴스의 프로퍼티 변수 간에 강한 상호 참조 발생합니다.
john = nil

/*
메모리 사이클 발생시 프로퍼티 간 상호 참조 때문에 두 인스턴스 모두를 nil로 해도 
deallocation 되지 않아 deinit이 호출되지 않습니다.
*/
unit4A = nil 

 

위의 예시는 두 class의 인스턴스 객체가 각각 지닌 프로퍼티 apartment 및 tenant가 서로 상대방 인스턴스를 참조함으로써 strong reference cycle 발생한 경우입니다.. (상호 참조가 발생한 경우)

reference count가 0이 되지 못하게 되어 프로그램이 실행되는 동안 두 인스턴스는 메모리에서 영영 빠져나오지 못합니다.

이와 같이 상호 참조의 문제를 해소하기 위해서는 클래스 프로퍼티 선언 키워드(var/ let) 앞에 weak 또는 unowned 키워드를 추가해야 합니다.

 

 

 

 

약한 참조란 (Weak Reference)

약한 참조는 강한 참조와 달리 참조 횟수를 증가시키지 않습니다.

변수의 선언 앞에 weak 키워드를 써주어서 사용합니다.

약한참조는 상수로 쓰일 수 없습니다.

nil이 할당될 수 있어야 하기 때문에 항상 옵셔널 타입입니다.

class 프로퍼티를 weak var로 선언하여 메모리 사이클을 막을 수 있습니다.

weak키워드는 상호 참조가 발생하는 양쪽에 모두 선언할 필요는 없습니다.

내 안에서 상대를 nil 시키는 것이므로 더 오래 머무르는 class의 선언하면 됩니다.

 

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

class Apartment {
    let unit: String
    weak var tenant: Person?
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person? = Person(name: "john appleseed") // Person -> 1
var home: Apartment? = Apartment(unit: "4A") //Apartment -> 1
john!.apartment = unit4A // Apartment ->2
home!.tenant = john // Person -> 1

/* 
인스턴스가 메모리에서 해제될 때, 
자신의 프로퍼티가 강한참조를 하고 있던 인스턴스의 참조 횟수를 1 감소시킵니다.
*/
john = nil // Person -> 0, Apartment-> 1 

print(home?.tenant) //nil
home = nil // Apartment-> 0

 

 

미소유 참조란 (Unowned Reference)

미소유 참조는 인스턴스의 참조 횟수를 증가시키지 않습니다.

unowned 키워드를 사용합니다.

weak은 꼭 옵셔널이어야 한다면 unowned은 옵셔널일 수도 있고 아닐 수도 있지만 nil일 경우 런타임 오류가 발생할 확률이 큽니다.

다만, 이렇게 reference counting을 하지 않는 상태에서 참조하는 인스턴스가 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: Int
    unowned let customer: Customer    
    init(number: Int, customer: Customer) {
        self.number = number
        self.customer = customer
    }

    deinit { print("Card  #\(number) is being deinitialized") }
}

var joo: Customer? = Customer(name: "joo") //Customer ->1
joo!.card = CreditCard(number: 123, customer: joo!) //CreditCard ->1, Customer->1
joo = nil //CreditCard ->0,Customer->0
//  prints "joo is being deinitialized"
// prints "Card #123 is being deinitialized"

 

위의 사례에서는 john이라는 Customer가 nil이 될 때에 이를 unowned로 참조하는 card도 같이 사라지므로 원본이 사라진 참조가 발생할 위험이 없습니다.

 

var john: Customer? = Customer(name: "john appleseed") //Customer 1
var card: CreditCard? = CreditCard(number: 123, customer: john!) //CreditCard 1, Customer 1

john!.card = card //CreditCard 2
john = nil //Customer 0, CreditCard 1
//아직 john을 참조하는 card가 있음에도 unowned이기 때문에 john은  deinit 가능

//하지만 이렇게 card참조하는 john 원본이 먼저 사라지게 되면 아래의 코드는 에러가 납니다.
print(card!.customer) 

 

위처럼 설계하게 되면 원본 사라진 참조 에러가 발생해버리는 일이 생기기 때문에 unowned를 사용할 때는 늘 주의가 필요합니다.

 

 

 

 

클로저의 강한 참조 순환

클래스와 마찬가지로 클로져도 강한 참조 순환이 발생할 수 있습니다. 클로져에서 self를 캡처해서 사용할 수 있는 값 획득 특성 때문이며 클로저가 클래스와 같은 참조 타입이기 때문입니다.

클로저가 인스턴스의 프로퍼티일 때 발생할 수 있는데 클로저 내부에서 self.property, self.method()처럼 인스턴스의 메서드나 프로퍼티를 접근할 때 발생할 수 있습니다.

클로저는 자신이 호출되면 언제든지 자신 내부의 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지하는데, 이때 자신을 프로퍼티로 갖고 있는 인스턴스의 참조 횟수도 증가시키면서 강한 참조 순환이 발생하게 됩니다.

클로저 내부에서 self 프로퍼티를 여러 번 호출하여 접근한다고 해도 참조 횟수는 한 번만 증가합니다.

 

HTMLElement 클래스 내부의 asHTML은 클로져가 할당되어있다습니다.

이 클로져 안에 클래스의 파라미터를 사용하기 위해서 self를 지정해서 사용하고 있습니다.

paragraph 변수에 HTMLElement 인스턴스를 초기화 해준 후 asHTML 클로져를 사용해서 강한 참조 순환이 일어났습니다.

그러므로 paragraph 가 nil이 되었더라도 강한 참조 순환으로 인해 메모리가 해제되지 않습니다.

 

class HTMLElement {
    let name: String
    let text: String?
    lazy var asHTML: () -> String = {
       if let text = self.text {
           return "<\(self.name)>\(text)<\(self.name)>"
       } else {
           return "<\(self.name) />"
       }
   }

   init(name:String, text:String? = nil) {
       self.name = name
       self.text = text
   }

   deinit {
       print("\(name) 디이니셜 라이즈 됨 ")
   }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello world")
print(paragraph!.asHTML())
//출력: <p>hello world<p>

paragraph = nil //메모리가 해제되지 않아 출력이 없다.

 


클로져 강한 참조 순환 문제 해결방안

클로져 내부에서 self를 캡처해서 사용하기 때문에 self를  약한 참조 혹은 미소유 참조로 지정해주면 된다.

클로져의 파라미터 앞에 소괄호를 사용해서 캡쳐 대상에 대한 참조 타입을 지정한다.

 

lazy var someClosure: () -> String = {
    [unowned self] in
    //코드 블럭
}


위 HTMLElement 클래스 코드에 미소유 참조를 지정해서 메모리
해제되도록 변경해 보았습니다.

앞선 HTMLElemnet클래스에서는 asHTML의 클로져에 캡처 대상인 self의 참조 타입을 지정해주지 않았습니다.

[unowned self]로 미소유 참조를 self에 지정해 주어 인스턴스가 nil이 되었을 때 메모리가 해제됩니다.

클로져에서 캡처해서 사용할 대상이 절대 nil 이 될 수 없는 값이라면 미소유 참조를 사용해야 합니다.

 

class HTMLElement {
    let name: String
    let text: String?
    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)<\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name:String, text:String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) 디이니셜라이즈 됨 ")
    }
 }

 var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello world")
 print(paragraph!.asHTML())
//출력: <p>hello world<p>

 paragraph = nil //출력: p 디이니셜라이즈 됨

 

 

 

#swift 기초

#swift 강의

#ios programming

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

[swift] try? try! try  (0) 2019.08.21
[swift] 스위프트의 특징  (0) 2019.08.21
[swift] Closure, Capture list  (0) 2018.04.02
[swift] Mutating, Memberwise Initializers, Access Control  (0) 2018.03.13
[swift] Property  (0) 2018.03.12