본문 바로가기

문법

[Swift] ARC에 대해 간단히 알아보잡(feat. 클로저캡쳐)

ARC(Automatic Reference Counting)에대해 알아보기!

[ARC동작]

  • 클래스의 새 인스턴스를 만들때마다 ARC는 인스턴스 정보를 담는데 필요한 적정한 크기의 메모리를 할당한다.
  • 메모리는 인스턴스에 대한 정보와 관련된 저장프로퍼티 값도 갖는다.
  • 인스턴스가 더이상 사용되지않을때 ARC는 그 인스턴스가 차지하고 있는 메모리를 해지해서 다른 용도로 사용할 수 있도록 공간을 확보한다.
  • 만일 메모리에서 해제된 인스턴스 프로퍼티에 접근한다면 Crash가 발생하게된다.
  • ARC에서 사용중인 인스턴스에 대한 참조를 추적함으로써 아직 사용중인 인스턴스를 해지하지않도록 한다.

RC란?

  • class를 참조하는 인스턴스의 수!

RC가 어떻게 적용되는지 알아보자

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

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

reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil

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

reference1~3이라는 변수가 Person의 타입을 가지기때문에 Person의 RC는 3이다. 그런데
reference1,reference2에 nil을 할당했을때 deinit은 발동하지않고 reference3까지 nil을
할당해야만 Person의 RC는 0이되므로 deinit이 동작한다.
(deinit은 Rc가 사라지는 과정!)

그런데 이런상황에서 ARC가 메모리관리를 해제해주지만 해제를 못하는 경우가 강한순환참조이다. 강순참

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: Person2?
    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은 Person를 참조하고있고 unit4A는 Apartment를 참조하고있다.
그리고 john내부의 apartment 프로퍼티는 unit4A를 참조하고 있고 동시에 unit4A도 내부의
tenant프로퍼티도 john을 참조하는 즉, 내부에서 서로를 참조하고있는 것이다.

john = nil
unit4A = nil

이경우에는 두  인스턴스에 nil을 할당해도 내부의 프로퍼티가 서로를 참조하고있기때문에 deinit이 동작하지않는다! 즉 메모리 누수가 발생한것인데 나중에 이러한 메모리누수가 내가 개발한 앱에 계속 쌓인다면??
사용하지도않는 기능이 메모리를 잡아먹고있을 수 있어 문제가 된다. 이는 어떻게해결할 수 있을까 ?

강참순을 해결하는 방법

weak와 unowned를 사용하는것이다. 이 두 기능모두 ARC에서 참조횟수를 증가시키지 않고 인스턴스를 참조한다.

weak는 참조하고있는 인스턴스가 먼저 메모리에서 해제될때 사용하고 unowned는 반대로 참고하고 있는 인스턴스가 같은 시점 혹은 더 뒤에 해제될때 사용한다.

약한참조(weak)

약참을 선언하면 참조하고있는것이 먼저 메모리에서 해제되기떄문에 ARC는 weak로 선언된 참조대상이
해지되면 런타임에 자동으로 참고하고있는 변수에 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

이전 예제외 다른점은 Apartment의 tenant변수가 Person인스턴스를 weak 즉, 약한참조로 참조하고있다는점이다. 그래서 이시점에서는 Person인스턴스에 대한 참조횟수는 변수 john이 참조하고 있는 횟수 하나 뿐이다. (tenant에대한 RC는 증가하지않음)
 
john2 = nil // John Appleseed is being deinitialized
// Apartment2의 tenant변수가 weak로 설정되있어 RC가 증가하지않았기때문에 nil을 할당하면 메모리 해제가 된다.
unit4A2 = nil // Apartment 4A is being deinitialized
// 강한순환참조가 발생하지않기떄문에 unit4A2에 nil을 할당하면 deinit이 동작하는걸 볼 수 있따.
 

전예제에서는 해제되지않았던문제를 weak로 해결한것이다.

미소유참조(unowned)

미소유참조는 weak와 다르게 참조대상이되는 인스턴스가 현재 참조하고있는것과 같은 생애주기를 갖거나 더긴 생애주기를 갖기때문에 항상 값이 있음을 예상할때 사용한다. 그래서
ARC는 미소유 참조에는 절대 nil을 할당하지 않는다. 다시말해 미소유는 옵셔널타입을 사용하지않는다.

#중요#

unowned는 참조대상 인스턴스가 항상 존재한다고 생각하기때문에 만약 미소유 참조로 선언된 인스턴스가 해제됐는데 접근하게되면 런타임에러가 발생한다.

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

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인스턴스를 참조하고 있다. customer는 미소유 참조 unowned로 선언한다. 이유는 고객과 신용카드를 비교해봤을때 신용카드는 없더라도 사용자는 남아있을 것이기때문이다. 다시말해
사용자는 항상 존재한다. 그래서 CreditCard에 customer를 unowned로 선언한다.

var quokka: Customer?

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

quokka = nil
// print' quokka is being deinitlalized
// print' Card #1234567890123456 is being deinitialized


## 미소유 참조와 암시적 옵셔널 프로퍼티 언래핑
weak와 unowned의 구분은 해다 참조가 nil이 될수 있느냐 없느냐로 구분된다.
하지만 이 두경우를 제외한 제 3의 경우도 발생할 수 있다.
두 프로퍼티가 항상 값을 갖지만 한번 초기화하면 절대 nil이 되지않는 경우이다.
이 경우에는 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 언래핑을 사용해 참조 문제를 해결할 수 있다.
 
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: String, country: Country) {
        self.name = name
        self.country = country
    }
}
 

위 capitalCity는 강제언래핑이기때문에 초기회되는 시점에 Country에 name이 초기화되는 시점에
self.capitalCity를 사용할 수 있게된다. 그리고 City는 강순참을 피하기위해 미소유참조 country를
선언해 두 인스턴스를 문제없이 사용할 수 있다.

var country = Country(name: "Korea", capitalName: "Seoul")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")

 

클로저에서 발생하는 강한순환참조

강참순은 클로저에 관계에서도 발생할 수 있다. 왜냐하면 클로저에서는 self를 캡쳐하기때문이다.
이 문제를 해결하기 위해서는 클로저 캡쳐리스트를 사용한다. 예제를 봐보자.
아래 HTMLElement클래스이 클로저 asHTML는 입력 값을 받지 않고 반환값이 String 인 () -> String
클로저를 사용한다. 그리고 클로저안에서 self.text와 self.name과 같이 self를 캡쳐하게된다.

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) is being deinitialized")
    }
}
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)> \(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML()) //<h1> some default text<h1>

 

asHTML 클로저는 지연프로퍼티로 선언됐다. 왜냐하면 HTML를 렌더링 하기위해 필요한 태그와 텍스트가
준비되고나서야 그것의 HTML이 필요하기때문이다. 또 지연 프로퍼티이기때문에 프로퍼티안에서 self를 참조할 수 있다.

이예제는 아직 이해했다라는 느낌보단 이런게 있구나 정도로 느껴진다. 클로저가 참조되는 방식이 아직 이해가가지않는것같아 어렵다 ㅠㅠ

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

여기서 잠깐! 클로저 캡쳐가 뭔데?

클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 즉. 원본값이 사라져도 클로져의 body안에서
그값을 활용할 수 있다. Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩함수이다.
중첩함수는 함수의 body에서 다른함수를 호출하는 형태로 된 함수이다. 예제를 살펴보자

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())// 10
print(incrementByTen())// 20
print(incrementByTen())// 30
 

incrementer메서드는 runningTotal와 amount를 갖지않는데도 실행이가능한 이유는 캡쳐를 하기떄문이다. 함수가 각기 실행되지만 실제로는 변수 runningTotal과 amount가 캡쳐링 되서 그변수를 공유하기때문에 계산이 누적된 결과를 갖는것이다.

let incrementBySeven = makeIncrementer(forIncrement: 7)
print(incrementBySeven())// 7

// 여기서 이전의 클로저를 실행한다면 ?
print(incrementByTen()) // 40
 
//func makeIncrementer(forIncrement amount: Int) -> Int {
//    var runningTotal = 0
//    func incrementer() -> Int {
//        runningTotal += amount
//        return runningTotal
//    }
//    return incrementer()
//}
//
//print(makeIncrementer(forIncrement: 10))// 10
//print(makeIncrementer(forIncrement: 10))// 10
//print(makeIncrementer(forIncrement: 10))// 10
 

서로 다른 변수에저장된 클로저이기때문에 연산에 영향이 없다. 그냥 다른 저장소의 변수를 사용해 계산하는것이다.
보통 함수는 실행후 stack에서 메모리가 해제가되기때문에 값이 유지가 되지않는다 그래서 주석으로 작성한 예시를 보면 함수는 동일하게 1,2,3 print()된 makeIncrementer값은 10,10,10 이 나오는것인데
클로저를 사용하게되면 캡쳐를 사용하기때문에 실행되면서 캡쳐된 변수의 값을 참조하게되는것이다.
그래서 불변인 let으로 선언되었는데도 값이 변할 수 있는 것이다.

연산결과를 살펴보면 첫번째 실행함수10이고 10
고로 첫번째 incrementByTen클로저를 실행하면 runningTotal에 10이 담기고
캡쳐가된 runningTotal = 10상태에서 두번째 클로저를 실행하니 amount 10이라서 값이 총 20이 호출되고
동일하게 runningTotal = 20인 상태에서 마지막 클로저를 실행하이 10을 더해 30이 되는것이다.

 

- Reference

- https://jusung.gitbook.io/the-swift-language-guide/language-guide/23-automatic-reference-counting#resolving-strong-reference-cycles-for-closures

 

자동 참조 카운트 (Automatic Reference Counting) - The Swift Language Guide (한국어)

이 경우 reference2, reference3모두 처음에 reference1이 참조하고 있는 같은 Person인스턴스를 참조하게 됩니다. 이 시점에 Person인스턴스에 대한 참조 횟수는 3이 됩니다. 그리고 나서 reference1, reference2두

jusung.gitbook.io

- https://jusung.gitbook.io/the-swift-language-guide/language-guide/07-closures#capturing-values

 

클로저 (Closures) - The Swift Language Guide (한국어)

문맥에서 타입 추론 (Inferring Type From Context)

jusung.gitbook.io