티스토리 뷰

Swift/야곰의 스위프트

27장 ARC

할루루 2018. 5. 1. 14:37

[시작]


- 참조 타입의 경우 하나의 인스턴스가 참조를 통해 여러 곳에서 접근되므로 메모리 해제 시점이 매우 중요하다.

- 인스턴스가 적절한 시점에 메모리에서 해제되지 않으면 메모리 자원을 낭비하게 되며, 이는 성능 저하로 이어진다.

- Swift는 프로그램 메모리 사용을 관리하기 위해 ARC(Automatic Reference Counting)라는 것을 사용한다.



[ARC란]


- Automatic Reference Counting

- 자동으로 메로리를 관리해줌

- 더이상 필요하지 않은 클래스의 인스턴스를 메모리에서 해제하는 식으로 작동한다.


- 가바지 컬렉션 기법과의 차이

 : ARC는 컴파일 시 참조를 계산하므로, 인스턴스가 언제 메모리에서 해제될지 예측하기 쉽고 메모리 관리를 위한 추가 자원을 할당할 필요가 없으나, ARC의 작동 규칙을 모르고 사용하면 인스턴스가 메모리에서 영원히 해제되지 않을 가능성이 있다.

 : 가비지 컬렉션은 실행 시 동적으로 참조를 계산하므로, 복잡한 상황에서 인스턴스를 해제할 수 있는 가능성이 더 높으나, 메모리 감시를 위한 추가 자원이 필요하여 성능 저하가 일어날 수 있고, 정확히 언제 메모리에서 해제될지 알기 힘들다.


- 클래스의 인스턴스를 생성할 때마다 ARC는 그 인스턴스에 대한 정보를 저장하기 위한 메모리 공간을 따로 할당한다.

 : 그 메모리 공간에는 인스턴스의 타입 정보, 인스턴스의 프로퍼티 등을 저장한다.

 : 인스턴스가 더 이상 필요 없는 상태가 되면 ARC가 메모리에서 인스턴스를 해제한다.

- 더 사용해야 하는 인스턴스를 해제한다면 오류가 일어날 수 있으므로, 이러한 상황을 ARC는 계속해서 추적한다.

 : ARC는 인스턴스가 메모리에서 해제되지 않도록 인스턴스 참조 여부를 계속해서 추적한다.

 : 어느 한 곳에서 해당 인스턴스를 참조하고 있다면 ARC가 그 인스턴스를 해제하지 않고 유지해야 하는 명분이 된다.

 : 인스턴스를 메모리에 유지시키려면 이러한 명분을 ARC에 제공해야 한다.



[강한참조]


- 인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어 주는 것.

- 강한참조를 사용하면 참조 횟수가 1 증가하며, 강한참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당하면 원래 자신에게 할당되어 있던 인스턴스의 참조 횟수가 1 감소한다.

 : 인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되며, 메모리에서 해제되기 직전 소멸자를 호출한다. (deinit)


- 참조의 기본은 강한참조이므로 별도 식별자를 명시하지 않으면 강한참조를 한다.



[강한참조 - 강한참조 순환 문제]


- 인스턴스끼리 서로가 서로를 강한참조할 때, 강한참조 순환이 일어난다.


class Person {

    let name: String

    init() {

        self.name = "Presto"

    }

    var room: Room?

    deinit {

        print("Person Deinitialized")

    }

}

class Room {

    let number: String

    init() {

        self.number = "ABC"

    }

    var host: Person?

    deinit {

        print("Room Deinitialized")

    }

}


var presto: Person? = Person()  //Person 인스턴스 참조 1

var room: Room? = Room()        //Room 인스턴스 참조 1

presto?.room = room             //Room 인스턴스 참조 2

room?.host = presto             //Person 인스턴스 참조 2

presto = nil                    //Person 인스턴스 참조 1

room = nil                      //Room 인스턴스 참조 1

//소멸자 호출되지 않음

//메모리에서 해제되지 않음


PersonRoom의 인스턴스에 접근할 방법은 사라졌으나 인스턴스 자체는 메모리에서 해제되지 않고 남아있게 되었다.

이처럼 두 인스턴스가 서로를 참조하는 상황에서 강한참조 순환 문제가 발생할 수 있다.


class Person {

    let name: String

    init() {

        self.name = "Presto"

    }

    var room: Room?

    deinit {

        print("Person Deinitialized")

    }

}

class Room {

    let number: String

    init() {

        self.number = "ABC"

    }

    var host: Person?

    deinit {

        print("Room Deinitialized")

    }

}


var presto: Person? = Person()  //Person 인스턴스 참조 횟수 1

var room: Room? = Room()        //Room 인스턴스 참조 횟수 1

presto?.room = room             //Room 인스턴스 참조 횟수 2

room?.host = presto             //Person 인스턴스 참조 횟수 2

presto?.room = nil              //Room 인스턴스 참조 횟수 1

room?.host = nil                //Person 인스턴스 참조 횟수 1

presto = nil                    //Person 인스턴스 참조 횟수 0

room = nil                      //Room 인스턴스 참조 횟수 0

//Person 클래스의 소멸자 호출됨

//Room 클래스의 소멸자 호출됨


이처럼 일일히 참조 횟수를 계산해가면서 코드를 작성할 수도 있겠으나, 좋은 방법은 아닐 것이다.



[약한참조]


- 약한참조는 강한참조와 달리 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다.

- weak 키워드를 사용하여 명시된 프로퍼티나 변수는 자신이 참조하는 인스턴스를 약한참조한다.

- 자신이 참조하는 인스턴스가 메모리에서 해제될수도 있음을 프로그래머가 예상할 수 있어야 한다.

 : 그 인스턴스를 강한참조하던 프로퍼티나 변수의 참조 횟수를 감소시켜 0으로 만들면 그 인스턴스가 메모리에서 해제되기 때문.

- 약한참조에는 nil이 할당될 수 있어야 하므로 상수에서는 사용될 수 없다.

 : 옵셔널 변수만 약한참조할 수 있다.


class Person {

    let name: String

    init() {

        self.name = "Presto"

    }

    var room: Room?

    deinit {

        print("Person Deinitialized")

    }

}

class Room {

    let number: String

    init() {

        self.number = "ABC"

    }

    weak var host: Person?

    deinit {

        print("Room Deinitialized")

    }

}


var presto: Person? = Person()  //Person 인스턴스의 참조 횟수 : 1

var room: Room? = Room()        //Room 인스턴스의 참조 횟수 : 1

room?.host = presto             //Person 인스턴스의 참조 횟수 : 1 | host가 약한참조

presto?.room = room             //Room 인스턴스의 참조 횟수 : 2 | room이 강한참조


presto = nil                    //Person 인스턴스의 참조 횟수 : 0, Room 인스턴스의 참조 횟수 : 1 | 메모리 해제

//인스턴스가 메모리에서 해제될 때, 자신의 프로퍼티가 강한참조를 하던 인스턴스의 참조 횟수를 1 감소시킨다!

print(room?.host)               //nil


room = nil                      //Room 인스턴스의 참조 횟수 : 0 | 메모리 해제


인스턴스가 메모리에서 해제될 때, 해제되는 인스턴스의 프로퍼티가 강한참조를 하던 인스턴스의 참조 횟수를 1 감소시킨다.



[미소유참조]


- 미소유참조(Unowned Reference) 또한 인스턴스의 참조 횟수를 증가시키지 않는다.

- 약한참조와는 다르게 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 작동한다.

 : 인스턴스가 메모리에서 해제되더라도 스스로 nil을 할당해주지 않는다.

 : 미소유참조를 하는 변수나 프로퍼티는 옵셔널이나 변수일 필요가 없다.

- 미소유참조를 하면서 인스턴스가 메모리에서 해제되고 그것에 접근하려고 하면 오류가 발생하므로 참조하는 동안 해당 인스턴스가 메모리에서 해제되지 않으리라는 확신이 있을 때만 사용해야 한다.

- unowned 키워드를 사용하여 명시된 프로퍼티나 변수(상수)는 자신이 참조하는 인스턴스를 미소유참조한다.


class Person {

    let name: String

    //카드를 소지할 수도, 소지하지 않을 수도 있다 -> 옵셔널

    //카드를 한 번 가진 후 잃어버리면 안되므로 강한참조

    var card: CreditCard?

    init(name: String) {

        self.name = name

    }

    deinit {

        print("\(name) Deinitialized")

    }

}

class CreditCard {

    let number: UInt

    //카드는 소유자가 분명히 존재해야 한다.

    unowned let owner: Person

    init(number: UInt, owner: Person) {

        self.number = number

        self.owner = owner

    }

    deinit {

        print("Card #\(number) Deinitialized")

    }

}


//Person 인스턴스의 참조 횟수 : 1

var presto: Person? = Person(name: "Presto")

if case let person? = presto {

    //CreditCard 인스턴스의 참조 횟수 : 1

    person.card = CreditCard(number: 123, owner: person)

    //Person 인스턴스의 참조 횟수 : 1 | 미소유참조

}

presto = nil

//Person 인스턴스의 참조 횟수 : 0

//CreditCard 인스턴스의 참조 횟수 : 0


사람이 카드를 소유하고 있다가 사람이 죽으면 카드도 없어지는 것이다!!!!!!

이를 미소유참조로 표현하면서 강한참조 순환 문제도 함께 해결하였다.



[미소유참조와 암시적 추출 옵셔널 프로퍼티]


class Company {

    let name: String

    var ceo: CEO!

    init(name: String, ceoName: String) {

        self.name = name

        self.ceo = CEO(name: ceoName, company: self)

    }

    func introduce() {

        print("\(name)의 CEO는 \(ceo.name)입니다.")

    }

}


class CEO {

    let name: String

    unowned let company: Company

    init(name: String, company: Company) {

        self.name = name

        self.company = company

    }

    func introduce() {

        print("\(name)는 \(company.name)의 CEO입니다.")

    }

}


let company: Company = Company(name: "회사", ceoName: "이름")

company.introduce()

company.ceo.introduce()


...어렵다...

미소유참조는 약한참조를 사용할 수 없는 경우(옵셔널이 아니어야 하거나 상수로 지정해야 하는 경우)에 강한참조를 피하기 위해 사용 가능하다.



[클로저의 강한참조 순환]


- 강한참조 순환 문제는 클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 발생하기도 한다.

 : 클로저 내부에서 self.ㅁ 처럼 인스턴스의 프로퍼티에 접근하거나 인스턴스의 메소드를 호출할 때 값 획득이 발생할 수 있는데, 이 경우 클로저가 self를 획득하므로 강한참조 순환이 발생한다.

- 클로저의 획득 목록을 통해 클로저의 강한참조 순환 문제를 해결할 수 있다.


class Person {

    let name: String

    let hobby: String?

    lazy var introduce: () -> String = {

        var introduction: String = "My name is \(self.name)."

        guard let hobby = self.hobby else { return introduction }

        introduction += " "

        introduction += "My hobby is \(hobby)."

        return introduction

    }

    init(name: String, hobby: String? = nil) {

        self.name = name

        self.hobby = hobby

    }

    deinit {

        print("\(name) is being deinitialized")

    }

}


var presto: Person? = Person(name: "Presto", hobby: "What")

print(presto?.introduce())

presto = nil


introduce 프로퍼티를 지연 저장 프로퍼티로 정의했기 때문에 내부에서 self를 통해 접근할 수 있다.

(클로저 내부에서 호출하는 self 프로퍼티는 참조 횟수를 증가시키지 않는다.)


- 클로저는 자신이 호출되면 언제든지 자신 내부의 참조를 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지한다.

- 이 때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킨다.

- 이리하여 위의 코드에서 소멸자는 호출되지 않는다.



[클로저의 강한참조 순환 해결 - 획득목록]


- 위의 문제를 획득목록Capture List을 통해 해결할 수 있다.

 : 클로저 내부에서 참조 타입을 획득하는 규칙을 제시해줄 수 있는 기능

- 획득목록은 클로저 내부의 매개변수 목록 이전 위치에 작성해준다.

- 참조 방식과 참조할 대상을 대괄호로 둘러싼 목록 형식으로 작성하며 획득목록 뒤에는 in 키워드를 써준다.

- 획득목록에 명시한 요소가 참조 타입이 아니라면 해당 요소들은 클로저가 생성될 때 초기화된다.


var a = 0

var b = 0

let closure = { [a] in

    print(a, b)

    b = 20

}

a = 10

b = 10

closure()   //0 10

print(b)    //20


- 획득목록에 명시한 요소가 값 타입일 때 : 

- 변수 a는 클로저의 획득목록을 통해 클로저가 생성될 때 값 0을 획득하였으나 변수 b는 따로 값을 획득하지 않았다.

- a는 클로저가 생성되었을 때 획득한 값을 갖지만, b는 변경된 값을 사용하고 있다.

- a 변수는 클로저가 생성됨과 동시에 획득목록 내에서 다시 a라는 이름의 상수로 초기화된 것이므로 외부에서 a의 값을 변경하더라도 클로저의 획득목록을 통한 a와는 별개가 된다.


class SimpleClass {

    var value: Int = 0

}

var x = SimpleClass()

var y = SimpleClass()


let closure = { [x] in

    print(x.value, y.value)

}

x.value = 10

y.value = 10

closure()   //10 10


- 획득목록에 명시한 요소가 참조 타입일 때 :

- 값 타입과는 다르게 서로 같게 동작한다.

- 참조 타입은 획득목록에서 어떤 방식으로 참조할 것인지 정해줄 수 있다.

 : 강한획득 / 약한획득 / 미소유획득

- 획득의 종류에 따라 참조 횟수를 증가시킬지 결정할 수 있다.

- 약한획득을 할 경우 획득목록에서 획득하는 상수가 옵셔널 상수로 지정된다.


class SimpleClass {

    var value: Int = 0

}

var x: SimpleClass? = SimpleClass()

var y = SimpleClass()


let closure = { [weak x, unowned y] in

    print(x?.value, y.value)

}

x = nil

y.value = 10

closure()    //nil 10


x는 약한참조 하였으므로 인스턴스가 메모리에서 해제되면 클로저 내부에서도 참조가 불가능하다.

y는 미소유참조 하였으므로 클로저가 참조 횟수를 증가시키지는 않지만, 메모리에서 해제된 상태에서 사용하려 했다면 애플리케이션이 강제종료 될 것이다.


class Person {

    let name: String

    let hobby: String?

    lazy var introduce: () -> String = { [unowned self] in

        var introduction: String = "My name is \(self.name)."

        guard let hobby = self.hobby else { return introduction }

        introduction += " "

        introduction += "My hobby is \(hobby)."

        return introduction

    }

    init(name: String, hobby: String? = nil) {

        self.name = name

        self.hobby = hobby

    }

    deinit {

        print("\(name) is being deinitialized")

    }

}

var presto: Person? = Person(name: "Presto", hobby: "What")

print(presto?.introduce())

presto = nil


self를 미소유참조하여 클로저의 강한참조 순환 문제를 해결하는 코드.

self 프로퍼티를 미소유참조한 것은, 해당 인스턴스가 존재하지 않는다면 프로퍼티도 호출할 수 없으므로 self는 미소유참조를 하더라도 실행 중에 오류를 발생시킬 가능성이 거의 없다고 볼 수 있기 때문이다.

하지만 역시 문제가 발생할 수 있는 상황이 있으며, 그러므로 미소유참조는 신중히 사용해야 하고, 약한참조를 사용한 처리도 괜찮다.



아 어렵다!!!!!!!

'Swift > 야곰의 스위프트' 카테고리의 다른 글

2회독 정리  (0) 2018.10.03
26장 where 절  (0) 2018.04.30
25장 패턴  (0) 2018.04.28
24장 타입 중첩  (0) 2018.04.28
23장 프로토콜 지향 프로그래밍  (0) 2018.04.28
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함