티스토리 뷰

Swift

[Swift] ARC에 대해서 알아보자

희철 2022. 10. 18. 23:25

ARC란?

 

ARC란 Automatic Reference Counting으로 스위프트의 메모리 관리 기법이다.

 

ARC에 대해서 살펴보기 전에 잠시 참조 타입에 대해서 생각해보자.

 

클래스의 인스턴스(참조 타입)를 생성하게 되면 Heap이라는 메모리 공간에 인스턴스의 타입 정보, 저장 프로퍼티의 값 등이 저장되고, Stack이라는 공간에 메모리의 주소가 저장된다.

 

Heap에 대해서 간단히 얘기하자면..

더보기

프로그래머가 직접 할당/해제를 통해 관리하는 메모리 공간

 

런타임 시에 결정되기 때문에 데이터의 크기가 확실하지 않을 때 사용

 

참조 타입의 값들은 전부 힙에 할당됨

 

하지만 직접 할당/해제를 하므로 스택보다는 속도가 느림

 

이후에 인스턴스가 더 이상 필요없게되면 사용하던 메모리 공간(힙)을 다시 사용할 수 있도록 메모리에서 해제시킨다.

 

하지만 사용되지 않는 인스턴스가 적절한 시점에 메모리에서 해제되지 않고 남아있다면 한정적인 메모리 공간이 낭비되는 것이다.

 

이것을 메모리 릭(memory leak)이라고 한다.

 

이 얘기가 왜 나온지 대충 알 수 있을 것이다.

 

이것이 바로 ARC의 역할이다.

 

 

 

ARC가 나오기 이전에는 retain, release 명령어를 이용해 메모리를 수동으로 관리하는 MRC 방식을 이용했었다.

요런식으로 사용했던 것 같음. 코드를 모름..

retain을 사용하면 참조 횟수가 1 증가하고, release를 사용하면 참조 횟수가 1 감소하는데 만약 참조 횟수가 0이되면 해당 인스턴스는 더 이상 필요없다고 판단되어 메모리에서 해제되는 방식이었다.

지금도 옵젝씨 코드에 대해서는 ARC를 사용하지 않을 수 있음!

 

하지만 수동으로 메모리를 직접 관리하는 것은 힘들고, retain과 release 코드로 인해 시스템적인 자원을 더 써야했기 때문에 이를 해결해주기 위해 애플에서 ARC를 도입하였다.

시스템적인 리소스도 줄어들고 시간도 짧아졌다.

ARC도 MRC와 같은 방식이지만, 컴파일러가 컴파일 시점에 코드를 알아서 추가해주기때문에 따로 작성해줄 필요가 없다.

 

또한 컴파일 시점에 인스턴스의 해제 시점이 정해져있기 때문에 인스턴스의 메모리 해제 시점을 예측할 수 있다는 장점이 있다.

컴파일러가 적절하게 retain과 release코드를 이런식으로 추가해줌. WWDC

 

이게 왜 장점인지 다른 프로그래밍 언어의 메모리 관리 기법인 GC(Garbage Collection)와 비교해보자.

 

GC는 런타임 중에 참조 카운팅을 진행하며 메모리를 관리한다.

 

그렇기 때문에 런타임 중에 프로그램의 동작에만 리소스를 사용하는게 아니라 메모리 감시를 위한 추가적인 리소스가 필요하고, 이로 인해 성능 저하를 유발하기 때문에 컴파일 시점에 인스턴스의 메모리 해제 시점을 안다는 것은 ARC만의 중요한 특징이다.

 

하지만 ARC의 작동 규칙을 모르고 사용한다면 영원히 인스턴스가 메모리에서 해제되지 않을 수 있다.

 

이러한 문제점을 해결하기 위해 언제 메모리에서 해제될지 예측할 수 있도록 ARC에 적용되는 규칙들을 알아보자.

 

 

강한 참조

 

한 가지 경우를 생각해보자.

 

만약 아직 더 사용해야 하는 인스턴스를 메모리에서 해제시킨다면 인스턴스의 프로퍼티나 메서드를 사용할 수 없을 것이다.

 

이 상태에서 강제로 접근하게 된다면 앱이 강제로 종료될 것이다.

 

이처럼 인스턴스가 필요한 상황일때 ARC는 해당 인스턴스를 메모리에서 해제시키면 안된다.

 

여기서 강한 참조라는 개념이 등장한다.

 

인스턴스는 참조 횟수가 0이 되는 순간에 메모리에서 해제되는데, 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 강한 참조를 사용하면 참조 횟수가 1 증가한다.

 

그리고 강한 참조를 사용하는 프로퍼티나 변수, 상수 등에 nil을 할당해주면 참조 횟수가 1 감소한다(nil을 할당할때는 당연히 옵셔널 타입).

 

또한, 함수가 종료되면 함수 내에 선언된 지역함수가 참조하던 인스턴스의 참조 횟수가 1 감소한다.

앞서 말했던 retain과 release를 생각하면된다. 

 

참조의 기본은 강한 참조다.

 

즉, 평소에 신경쓰지 않고 별도의 식별자 없이 선언했던 클래스 타입의 변수, 상수 등은 강한 참조로 선언된 것임.

처음에 heehee에 할당된 Person 타입 인스턴스는 별도의 식별자가 없으므로 강한 참조로 선언된 것이다.

 

마찬가지로 heecheol과 yoon에서도 강한 참조로 할당되므로 인스턴스의 참조 횟수는 3일 것이다.

 

세 변수에 전부 nil을 할당해주면 인스턴스의 참조 횟수가 3이 줄어들 것이기때문에 0이되어 deinit의 print구문이 출력되는 것을 확인할 수 있다.

 

 

 

강한 참조 순환

 

 

이번엔 아래의 코드를 확인해보자.

class Person {
    let name: String
    init(name: String) { self.name = name}
    var owner: Pet?
    deinit { print("디이닛") }
}

class Pet {
    let name: String
    init(name: String) { self.name = name}
    var pet: Person?
    deinit { print("디이닛") }
}

var hee: Person? = Person(name: "heecheol") //Person 인스턴스 참조 횟수: 1
var dooboo: Pet? = Pet(name: "dooboo") //Pet 인스턴스 참조 횟수: 1

hee?.owner = dooboo //Pet 인스턴스 참조 횟수: 2
dooboo?.pet = hee //Person 인스턴스 참조 횟수: 2

hee = nil //Person 인스턴스 참조 횟수: 1
dooboo = nil //Pet 인스턴스 참조 횟수: 1

hee와 dooboo가 선언될 때 각각 Person과 Pet의 참조 횟수가 1 증가한다.

 

또한 각각의 저장 프로퍼티인 owner와 pet에 Person과 Pet 인스턴스를 할당하므로 두 인스턴스의 참조 횟수는 2일 것이다.

 

우선 hee에만 nil을 할당했다고 생각해보자.

 

hee가 참조하던 Person인스턴스의 참조 횟수가 1이 될텐데 아직, 이 인스턴스를 참조할 방법은 dooboo의 pet밖에 남지 않았다.

 

이때, dooboo도 nil이 된다면 서로가 참조하던 두 인스턴스는 아직 참조 횟수가 1인데 더 이상 접근할 방법이 없어지게 되는 것이다.

Person과 Pet은 각각 참초 횟수가 2인데, hee와 dooboo에 nil을 할당해도 1씩 남아있어 메모리에서 해제되지않음.

ARC는 참조 횟수가 0이 아니라면 계속해서 메모리에 남기기 때문에 두 인스턴스가 차지하는 메모리는 평생 해제되지 않을 것이다.

 

이것을 강한 참조 순환(Strong reference cycle, 이전에는 retain cycle이라고했음)이라고 하며, 메모리 릭으로 이어지게 된다.

 

이 문제를 해결하기 위해서는 hee와 dooboo를 둘 다 nil로 할당하지 않고 아래와 같은 코드들로 해결할 수 있다.

hee = nil
dooboo?.pet?.owner = nil
dooboo = nil
hee?.owner = nil
hee = nil
dooboo = nil

하지만 코드 작성을 까먹거나, 해제해야 할 프로퍼티가 너무 많다면 위와 같은 방법으로 해결하기 힘들 수도 있다.

 

그래서 스위프트는 위와 같은 문제를 해결하기 위해 약한 참조(weak)와 미소유참조(unowned)라는 두 가지 방법을 제공해준다.

 

 

 

약한 참조(Weak)

 

 

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

 

프로퍼티나 변수 앞에 weak 을 붙이면 된다.

 

여기서 주의해야 할 것은 weak은 상수에서 쓰일 수 없다!!

 

약한 참조를 사용한다면 참조 중인 인스턴스가 메모리에서 해제될 수도 있는데, 이때 ARC는 약한 참조를 nil로 할당하기 때문에 값이 변경되어야 하므로 상수에서 쓰일 수 없는 것이다.

 

즉, 약한참조는 항상 옵셔널이어야 한다.

 

그럼 앞의 강한 참조 순환 문제를 약한참조를 이용해 해결해보자.

class Person {
    let name: String
    init(name: String) { self.name = name}
    weak var owner: Pet?
    deinit { print("디이닛") }
}

class Pet {
    let name: String
    init(name: String) { self.name = name}
    var pet: Person?
    deinit { print("디이닛") }
}

var hee: Person? = Person(name: "heecheol") //Person 인스턴스 참조 횟수: 1
var dooboo: Pet? = Pet(name: "dooboo") //Pet 인스턴스 참조 횟수: 1

hee?.owner = dooboo //Pet 인스턴스 참조 횟수: 1 => 2가 아님!!
dooboo?.pet = hee //Person 인스턴스 참조 횟수: 2

dooboo = nil
hee = nil

Person 클래스의 owner 프로퍼티가 약한 참조를 하도록 weak을 추가했다.

 

이제 owner프로퍼티에 dooboo가 참조하는 인스턴스를 참조하도록 할 때 참조 횟수가 증가하지 않게 된다.

 

그래서 dooboo에 nil을 할당하게 되면 dooboo가 참조하던 Pet 인스턴스의 참조 횟수가 바로 0이 되어 메모리에서 해제된다.

 

이로 인해, hee의 owner프로퍼티가 참조하던 인스턴스가 사라졌으므로 ARC는 owner에 nil을 할당하게 된다.

이후 hee에도 nil을 할당하면 dooboo가 nil이 되면서 참조 횟수가 감소되었던 것까지 총 2가 감소되어 0이 되므로 메모리에서 해제된다.

 

그리고 인스턴스끼리 참조를 하는 경우에 아무나 weak으로 선언하지말고 수명이 더 짧은 인스턴스를 참조하는 것을 약한 참조로 선언한다.

 

내가 작성한 예제말고(내가 두부의 펫이기때문에 정상적이 아님) 정상적인 Person과 Pet의 관계를 생각해보자.

 

일반적으로 사람이 반려동물을 기르는 형태이며, 사람의 수명이 더 길기 때문에 Person의 pet이 nil이 될 수 있다.

 

즉, Pet의 수명이 더 짧은 것이므로 Pet 인스턴스를 참조하는 hee의 pet을 약한참조로 선언하면된다.

 

 

 

미소유참조(Unowned)

 

 

미소유참조도 약한참조와 마찬가지로 인스턴스의 참조 횟수를 증가시키지 않는다.

 

미소유참조는 약한참조와 반대로 자신이 참조하는 인스턴스가 항상 메모리에 존재할거라고 확신하는 경우에만 사용된다.

 

다시 말해서 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 nil을 할당하지 않으며, 이는 런타임 에러까지 이어질 수도 있다.

 

그래서 상수, 옵셔널이 아닌 경우에도 사용할 수 있다.

(위에서 설명을 안했는데, 약한참조가 변수에만 사용이 가능한 이유는 참조하고 있는 인스턴스가 메모리에서 해제된 경우에 nil로 값이 변경되어야하기 때문이다.)

 

앞에 unowned 키워드를 써주면 변수, 상수나 프로퍼티는 자신이 참조하는 인스턴스를 미소유참조 할 수 있다.

 

공식문서에 있는 CreditCard와 Customer 예제를 통해 확인해보자.

 

은행에서 신용카드를 발급해주기 위해서는 고객이 반드시 필요하기때문에, 신용카드의 customer가 참조하는 Customer클래스는 반드시 메모리에 존재한다고 생각하고 unowned를 사용했다.

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
    deinit { print("Customer 디이닛") }
}

class CreditCard {
    let number: String
    unowned let customer: Customer
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("CreditCard 디이닛") }
}

var person: Customer? = Customer(name: "person") //Customer 참조 횟수: 1
if let person = person {
    //CreditCard 참조 횟수: 1
    person.card = CreditCard(number: "123123213213", customer: person) //Customer 참조 횟수 그대로
}
person = nil //이 사람이 죽으면 Customer 참조 횟수가 0이 되고, CreditCard 참조 횟수도 0

 

person이 선언되면서 Customer의 참조 횟수는 1이되고, person의 card 프로퍼티에 CreditCard를 할당하면서 CreditCard의 참조 횟수도 1이 될 것이다.

 

또한 CreditCard의 customer프로퍼티에도 person이 참조하는 Customer를 할당해준다.

 

하지만 미소유참조이기때문에 Customer의 참조 횟수는 그대로 1일 것이다.

 

이후에 person이 죽게되었다고 생각해보자.

 

그렇게 되면 person이 강한참조를 하고 있었지만 nil을 할당받아 Customer의 참조 횟수가 0이 되어 메모리에서 해제되고, 이로 인해 CreditCard의 참조 횟수도 0이 되어 마찬가지로 메모리에서 해제되어 강한참조 순환문제를 해결할 수 있다.

person이 사라지면서 참조 횟수가 0이 되어 메모리에서 해제

 

그럼 이번엔 비슷하지만 오류가 나는 경우를 생각해보자.

var person: Customer? = Customer(name: "person")
var card: CreditCard?
if let person = person {
    card = CreditCard(number: "123123123", customer: person)
    person.card = card
}
person = nil
print(card?.customer)

 card에 CreditCard를 할당시킨 상태에서 person에 nil을 할당해보았다.

 

unowned는 weak처럼 참조하던 인스턴스가 메모리에서 해제됐을 때 nil을 주지 않으므로 런타임 에러가 발생한다.

 

다시 한 번 말하지만 자신이 참조하는 인스턴스가 항상 메모리에 존재할거라고 확신하는 경우에만 사용해야 한다.

 

 

 

미소유 옵셔널 참조(unowned optional reference)

 

 

 

앞에서 미소유참조는 참조하는 인스턴스가 메모리에서 해제되더라도 nil값을 할당해주지 않는다고 하였다.

 

그렇다고 옵셔널에서 미소유참조를 사용하지 못하는 것이 아니다.

 

원래는 사용못했었지만 swift 5.0부터 바뀌었다.

 

즉, 미소유참조가 옵셔널타입도 지원한다는 얘기이다.

 

이를 미소유 옵셔널 참조라고 하는데 약한참조인 weak와 동일한 상황에서 사용할 수 있다고 한다.

 

하지만 미소유 옵셔널 참조는 항상 유효한 객체를 가리키거나, 아닌 경우에는 nil을 할당하도록 직접 신경써야한다.

 

공식문서에 있는 예시를 통해 간단하게 확인해보자.

//학과가 운영 과목들을 소유하고 있는 형태
class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}

//unowned인 department와 nextCourse는 각 과목이 소유한 것이 아님.
//각 과목은 특정 학과에 반드시 포함이 되어있기때문에 unowned이며 non optional 타입으로 선언
//모든 과목이 이후에 필수로 이수해야 되는 과목이 있는 것은 아님. 그래서 nextCourse는 optional 타입
class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}

주석에도 써있지만 다시 한 번 클래스의 구조를 훑어보자.

 

학과는 운영하는 과목들을 포함하고 있는 형태이기때문에 Course배열을 강한참조하고 있다.

 

각 과목은 반드시 어떤 학과에 소속되어 있는 과목이다.

 

즉, 과목이 소유하고 있지 않으며 반드시 값이 있기 때문에 미소유참조이며 옵셔널이 아닌 형태로 선언되었다.

 

또한 각 과목은 반드시 다음으로 수강해야 하는 과목이 있는 것이 아니며, 다음으로 수강해야하는 과목또한 과목이 소유하고 있는 형태가 아니다.

 

그래서 마찬가지로 미소유참조이지만 옵셔널 타입이다.

 

이로 인해 이니셜라이저에서 nil을 할당할 수 있는 것이다.

 

nil을 할당할 수 있다는 것에서 미소유참조와 차이점이 분명히 있지만, 참조하는 인스턴스가 메모리에서 해제될 때 ARC가 알아서 nil을 할당해주는 것이 아니므로 주의해야한다.

 

 

 

Delegate Pattern

 

 

delegate pattern을 사용할 때 메모리 릭을 경험했던 적이 있을 것이다.

 

이유는 지금까지의 경우와 같다.

 

delegate를 strong으로 선언하게 되면 뷰컨트롤러끼리 강하게 참조하게 된다.

 

이 상태에서 만약이라도 다른 곳에서 강한참조를 하게 된다면 참조 횟수가 0이 되지 못해 메모리에서 해제되지 않을 것이고, 이는 메모리 릭으로 이어질 것이다.

 

다시 말해서 delegate pattern을 사용하는 경우에 강한참조 순환 문제가 발생할 가능성이 있기 때문에 weak을 사용하는 것을 추천한다.

 

 

 

@IBOutlet weak var

 

 

스토리보드를 이용해 작업을 하다보면 IBOutlet을 이용해 뷰컨트롤러와 연결하는 경우가 많을 것이다.

storage 부분을 확인해보면 디폴트로 Weak이 설정되어있지만 Strong으로 바꿀 수도 있다.

 

그렇다면 왜 디폴트값은 Weak인 것일까

 

Strong으로 선언해도 상관은 없지만 문제는 메모리가 부족한 경우이다.

 

메모리가 부족해지면 didReceiveMemoryWarning메서드를 통해 메모리를 비워주는데, 이때 뷰가 메모리에서 해제되는 경우를 생각해보자.

 

기본적으로 뷰컨트롤러는 관리하는 뷰에 대해 강한참조를 유지하고, 그 뷰는 서브뷰들에 대해 강한참조를 유지한다.

 

근데 만약 IBOutlet을 선언할 때 강한참조를 하게 되면 해당 객체에 대해 참조 횟수가 1이 더 늘어난 상태일 것이다.

 

이때, 메모리가 부족하여 메모리에서 뷰를 해제시킨다면 뷰는 정상적으로 해제되겠지만 서브뷰들은 뷰컨트롤러와 강한참조를 유지하고 있기때문에 해제되지 않을 것이다.

(정확히 맞는지 모르겠음...)

 

물론 strong으로 선언해야 하는 특별한 경우도 있으므로 참고해야한다 :)

(여기선 스킵)

 

 

 

클로저의 강한참조 순환

 

 

지금까지 확인해본 것은 두 클래스 인스턴스 간의 강한 참조로 인해 발생한 강한참조 순환 문제이다.

 

근데 사실 나는 강한참조 순환 문제를 인스턴스끼리 참조해서 발생하는 것보다 클로저에서 경험한 것이 대부분이다.

 

클로저가 내부에서 인스턴스의 프로퍼티에 접근하는 경우나 메서드를 호출할 때 self를 캡쳐한다.

 

근데 클로저는 값을 캡쳐할 때 값 타입으로 가져오는 것이 아닌 캡쳐한 값을 참조한다.

 

클로저를 인스턴스 프로퍼티로 할당하면 클래스는 클로저를 참조하므로 결국 참조 타입끼리의 강한참조로 인해 문제가 발생한다.

 

간단한 코드로 강한참조 순환 문제를 확인해보자.

class Person {
    
    let name: String
    let age: Int
    
    lazy var sayHello: () -> () = {
        print("안녕 나는 \(self.name). 나이는 \(self.age)살")
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    deinit {
        print("Person deinit")
    }
}

var hee: Person? = Person(name: "희희", age: 100)

hee?.sayHello
hee = nil

sayHello 프로퍼티를 lazy로 선언했고, 클로저를 할당해주었다.

(클로저 내부에서 Person 인스턴스의 다른 인스턴스 프로퍼티에 접근하고 있는데, 전부 초기화가 된 이후에야 접근이 가능하므로 lazy로)

 

위의 코드를 실행하면 deinit 블럭 내의 print구문이 출력되지않는다.

 

sayHello를 호출하는 순간 클로저는 언제든지 내부의 참조들을 사용할 수 있도록 참조횟수를 증가시킨다.

 

문제는 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시키는 것이다.(기본적으로 강한참조)

그러다보니 hee에 nil을 할당하더라도 참조 횟수가 0이 되지 않아 메모리에서 해제되지 않는다.

 

이 문제도 앞에서 해결했던 것과 똑같이 weak과 unowned를 이용하지만 캡쳐리스트로 해결이 가능하다.

 

 

해결해보기 전에 캡쳐리스트(Capture list)에 대해서 잠깐 알아보자.

 

앞에서 클로저는 값을 캡쳐할때 캡쳐한 값을 참조한다고 했었다.

 

근데 캡쳐리스트를 사용하면 클로저를 선언할 때의 값을 상수로 캡쳐한다.

 

상수로 캡쳐하므로 당연히 클로저 내에서 값을 수정할 수 없다.

 

하지만 이것은 값 타입을 캡쳐할 때에 해당되고, 참조 타입을 캡쳐하는 경우엔 캡쳐리스트를 사용하더라도 이전과 동일하게 캡쳐한 값을 참조한다.

 

캡쳐리스트는 매개변수 목록 이전에 작성하며, 참조 방식과 참조 대상을 []로 묶어준다.

 

예시를 통해 확인해보자.

 

값 타입을 캡쳐한 경우이다.

var a = 0
var b = 0
let foo = { [a] in
    print(a, b)
    b = 20
}
a = 10
b = 10
foo() //0, 10
print(a) //10
foo() //그대로 a는 클로저가 선언될 때의 값인 0으로 캡쳐되어있음.

 

이번엔 참조 타입에 캡쳐리스트를 사용해보겠따.

class Test {
    var value = 0
}

var x = Test()
var y = Test()

let foo = { [x] in
    print(x.value, y.value)
}
x.value = 10
y.value = 10
foo()

캡쳐리스트를 통해 x를 캡쳐했지만 값 타입을 캡쳐했을 때와 달리 y와 동일하게 값이 변경된 것을 확인할 수 있다.

 

이는 x, y가 모두 참조 타입의 인스턴스가 있기 때문이다.

 

이렇게 참조 타입을 캡쳐하는 경우엔 캡쳐리스트에서 어떤 방식(strong, weak, unowned)으로 캡쳐할지 정할  수 있다.

 

이를 통해 참조 횟수를 증가시키지 않고 참조하여 클로저의 강한참조 순환 문제를 해결할 수 있다.

 

class Person {
    
    let name: String
    let age: Int
    
    lazy var sayHello: () -> () = { [weak self] in
    	guard let self = self else { return }
        print("안녕 나는 \(self.name). 나이는 \(self.age)살")
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    deinit {
        print("Person deinit")
    }
}

var hee: Person? = Person(name: "희희", age: 100)

hee?.sayHello
hee = nil

캡쳐리스트를 통해 인스턴스를 weak으로 캡쳐하면 참조 횟수가 증가하지않아 hee에 nil을 할당했을때 deinit이 정상적으로 출력된다.

 

unowned의 경우엔 옵셔널 바인딩도 필요 없이 weak과 같은 결과를 얻을 수 있다.

 

하지만 다른 곳에서 참조하고 있는 상황에 메모리에서 해제되면 오류가 발생하므로 신중하게 사용해야한다.

 

 

 

탈출 클로저(@escaping closure)

 

 

개발을 하다보면 매개변수로 클로저를 받는 경우를 굉장히 많이 봤을 것이다.

 

@escaping 키워드 여부에 따라 escaping closure와 non escaping closure로 나뉘는데, 함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출되는 경우에 @escaping 키워드를 붙여준다.

 

만약 @escaping을 사용하지 않는다면, 함수 내부에서만 쓰인다는 의미이기 때문에 인스턴스의 프로퍼티를 사용하기위해 self를 사용하지 않아도 되며, 따로 메모리 관리를 신경써서 해줄 필요가 없을 것이다.

 

하지만 탈출클로저의 경우는 함수 외부에서 사용될 수 있기때문에 self를 통해 값을 캡쳐하여 내부 프로퍼티에 접근해야한다.

(non escaping closure는 안써도 괜찮)

 

역시나 기본은 강한참조이기때문에 다음과 같은 경우를 신경써주지않으면 메모리릭이 발생하게 되는 것이다.

  • 프로퍼티에 저장이 되며, 클로저 내의 어떠한 객체라도 강한참조를 하고 있는 경우

 

프로젝트를 진행하며 직접 메모리 릭을 경험했던 코드를 통해 확인해보자.

let menus = [
    UIAction(title: AlertText.edit, image: UIImage(systemName: ImageName.pencil)) { _ in
        self.presentEditView()
    },
    UIAction(title: AlertText.delete, image: UIImage(systemName: ImageName.delete), attributes: .destructive) { _ in
        self.deleteMemory()
    }
]

let menuButton = UIBarButtonItem(title: nil, image: UIImage(systemName: ImageName.ellipsis), primaryAction: nil, menu: menu)

내 앱에서 작성된 기록을 누르면 디테일하게 보여주는 뷰(디테일 뷰라고 하겠음)를 띄우게 된다.

 

디테일 뷰의 바버튼에는 수정, 삭제 옵션이 있는 메뉴를 추가하였다.

 

근데 push했다가 pop을 하여도 메모리가 줄지않고 계속해서 늘어나는 것을 볼 수 있따..(관련 지식이 없어서 처음에 발견했을때 식은땀났음)

한줄한줄 주석처리해가면서 문제점을 확인해본 결과 메뉴쪽 코드가 문제인 것을 알 수 있었다.

 

지금이니까 한 번에 말할 수 있지만, 처음 발견했을때는 정말 몰랐었다.

 

아무튼 이유를 설명해보자면 UIAction의 handler부분은 탈출 클로저 형태이다.

위에서 말했지만, 탈출 클로저는 내부 프로퍼티에 접근하기 위해 self를 사용해야하는데 이때 강한참조로 캡쳐된다고 하였다.

 

강한참조와 함께 해당 클로저가 menu라는 프로퍼티에 사용되는 상태이므로 강한참조 순환 문제가 발생하는 것이다.

 

이로 인해, pop되어도 메모리에서 해제되고 있지 않기 때문에 메모리 릭이 발생한다.

 

weak과 unowned를 사용하면 해결할 수 있다!!

let menus = [
    UIAction(title: AlertText.edit, image: UIImage(systemName: ImageName.pencil)) { [weak self] _ in
        self?.presentEditView()
    },
    UIAction(title: AlertText.delete, image: UIImage(systemName: ImageName.delete), attributes: .destructive) { [weak self] _ in
        self?.deleteMemory()
    }
]

 

추가) 수업에서 디퍼블을 다루면서 dataSource쪽을 확인하는데, cellRegistration을 전역변수로 설정했을 때는 클로저 내에서 self로 접근하는 부분이 있었다.

 

cellProvider의 definition을 확인해보면 @escaping을 볼 수 있따.

 

dataSource라는 프로퍼티에 저장되고, self를 strong으로 캡쳐하고있기때문에 강한참조 순환 문제가 생길 수 있는 것이다.

 

이를 해결하기 위해서는 마찬가지로 [weak self]를 사용하거나, 수업 때처럼 cellRegistration을 지역변수로 설정하여 self를 참조하지 않게 만들어 해결할 수 있따.

 

탈출 클로저를 사용할 때는 메모리 릭의 가능성을 항상 생각하면서 사용하자

 

 

지금까지 ARC에 대해 알아보았다

 

ARC라는 주제는 굉장히 중요하므로 더 자세히 공부해보면 좋을 것 같다.

 

 

 

 


https://babbab2.tistory.com/m/83

 

Swift) 클로저(Closure) 정복하기(3/3) - 클로저와 ARC

안녕하세요 :) 소들입니다! 이번 포스팅은 클로저 정복하기 마지막 편!!! 메모리나 ARC에 대한 사전 지식이 없으면 조금 이해하기 어려울 수 있으니, 메모리 관련 포스팅을 먼저 보고 오심을 추천

babbab2.tistory.com

https://sujinnaljin.medium.com/ios-arc-뿌시기-9b3e5dc23814

 

Medium

iOS Interview: Singleton Design Pattern (Understanding, Implementation)

sujinnaljin.medium.com

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

 

Automatic Reference Counting — The Swift Programming Language (Swift 5.7)

Automatic Reference Counting Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management your

docs.swift.org

https://manasaprema04.medium.com/memory-management-in-swift-heap-stack-arc-6713ca8b70e1

 

Medium

iOS Interview: Singleton Design Pattern (Understanding, Implementation)

manasaprema04.medium.com

https://medium.com/flawless-app-stories/you-dont-always-need-weak-self-a778bec505ef

 

Medium

 

medium.com

http://monibu1548.github.io/2018/05/03/iboutlet-strong-weak/

 

Interface Builder IBOutlet연결에 Strong과 Weak 어떤것을 써야할까? - JingyuJung's Blog

IBOutlet의 Strong vs Weak Interface Builder를 사용하는 프로젝트에서 View를 코드상에서 제어하기 위해 IBOutlet으로 스토리보드 <-> 코드 를 연결하게 됩니다. Ctrl키를 누르고 View를 .m 또는 .h 파일로 가져오

monibu1548.github.io

https://soulpark.wordpress.com/2013/04/03/ios-automatic-reference-counting-arc/

 

[iOS] Automatic Reference Counting (ARC)

iOS 5부터 Apple이 LLVM Compiler 를 채용함에 따라 ARC (Automatic Reference Counting)가 iOS 개발에 적용되었다. Java나 C#만을 다룬 상태에서 iOS 4.x 후반대에 iOS 공부를 시작한 본인으로서는 개발 입문 후 곧 소

soulpark.wordpress.com

 

'Swift' 카테고리의 다른 글

[Swift] 접근 제어  (0) 2022.08.28
[Swift] Optional(옵셔널)  (0) 2022.07.07
[Swift] 튜플(Tuple)  (0) 2022.06.10
[Swift] 진수 변환(radix)  (0) 2022.06.06
[Swift] 순열과 조합(Permutation / Combination) 구현해보기  (0) 2022.05.10
댓글
최근에 올라온 글
Total
Today
Yesterday