아요 개발 일기
[Swift] Weak, Strong Reference (feat. ARC) 본문
안녕하세요 :)
저번에 ARC에대해 공부를 했었죠?
정말 좋은 ARC지만 강한 참조 순환 문제가 생길 수 있어요!
참조 순환이란, 어디에서도 참조가되지않는데도 메모리가 해제 되지 않는 경우를 말합니다
같이 해결 방법을 같이 알아볼까요??
이 글은 Swift Language Guide 기반으로 작성되었습니다 ☺️
강한 참조 순환이 발생하는 부분은
클래스와 클로저 두 가지에요!
클래스부터 알아봅시다 고고고구마
Strong Reference Cycles Between Class Instances
클래스 인스턴스간 강한 참조 순환
클래스 인스턴스간 강하게 상호 참조하는 경우에 강한 참조 순환이 발생하는데,
코드를 보면서 강한 참조 순환이 어떻게 발생하는지 알아봅시다!
위 코드를 보면 Person이라는 클래스는 변수로 Apartment 클래스의 인스턴스를 소유하고 있고
그 Apartment 클래스에서는 변수로 Person형의 인스턴스를 소유하고 있습니다.
서로 상호 참조하고 있는 모습이죠?
일단 각각 Person, Apartment 클래스 형의 변수를 생성해봅시다!
각각 변수에 맞는 타입의 인스턴스를 생성해줍니다
지금까지의 변수와 인스턴스 상태는 아래 그림과 같습니다.
Person과 Apartment의 인스턴스를 생성했으니,
Reference Count가 +1씩 됩니다!
여기서 sojin의 apartment 변수에 unit4A를 unit4A.tenant에 sojin을 할당해보겠습니다.
그럼 아래 그림과 같이 참조하는 상황이 됩니다.
이제는 각각 인스턴스의 참조 횟수가 2가되겠죠?
이 시점에서 각 변수에 nil을 할당해 참조를 해지해보겠습니다!
이렇게 하면 원래 Person, Apartment 인스턴스가 해지되야하지만..
두 인스턴스는 해지되지 않습니다ㅜㅜ
위 그림은 각 변수에 nil을 할당한 시점에서의 참조상황입니다!
변수 sojin과 unit4A는 각 인스턴스에 대한 참조를 하고 있지 않지만
Person인스턴스와 Apartment인스턴스의 변수가 각각 상호 참조를 하고 있어,
참조 횟수가 1이기 때문에 이 두 인스턴스는 해지되지 않고 메모리 누수가 발생합니다.
메모리 누수가 발생하지 않으려면
이런 강한 참조 순환 문제가 생기지 않도록 해야겠죠?
이번에는 문제 해결 방법을 알아보러 가봅시다!
Resolving Strong Reference Cycles Between Class Instances
클래스 인스턴스간 강한 참조 순환 문제 해결 방법
먼저 앞서 살펴본 강한 참조 순환 문제를 해결하기 위해서는
weak 참조와 unowned 참조를 사용하는 방법이있습니다!
이 두가지 방법은 ARC에서 참조 횟수를 증가시키지 않고 인스턴스를 참조하여
강한 참조 순환 문제를 해결합니다
1.
Weak References
약한 참조
약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에
ARC는 약한 참조로 선언된 참조 대상이 해지 되면
런타임에 자동으로 참조하고 있는 변수에 nil을 할당합니다.
코드로 알아봅시다!!
아까 본 코드에서 Apartment의 teant 변수를 weak로 선언해줍니다!
앞선 예제와 같이 Person 인스턴스와 Apartment 변수에서
각각 인스턴스를 상호 참조하도록 할당해줍니다
그럼 위와 같은 참조상황이 됩니다.
앞선 예제와 다른점은 weak로 참조하고 있는 것이죠?
맨 처음에 해당 방법들이 ARC의 참조 횟수를 증가시키지 않고 인스턴스를 참조한다고 설명했었습니다
그래서 이 시점에서 Person 인스턴스에 대한 참조 횟수는 변수 sojin이 참조하고 있는 1회 뿐입니다!
이렇게 sojin을 nil로 할당하면 더 이상 Person 인스턴스를 참조하는 것은 없게됩니다
그 결과 ARC에서 위 그림과 같이 Person 인스턴스를 메모리에서 해지합니다.
이 시점에 변수 unit4에 nil을 할당하면
Apartment 인스턴스를 참조하는 개체도 사라지게되어 Apartment 인스턴스도 메모리에서 해지됩니다.
2.
Unowned Reference
미소유 참조
미소유 참조는 약한 참조와 다르게
참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 생애주기(lifetime)를 갖거나
더 긴 생애 주기(longer lifetime)를 가질 때 사용합니다.
그래서 ARC는 미소유 참조에 옵셔널 타입을 사용하지 않습니다.
미소유 참조는 참조 대상 인스턴스가 항상 존재한다고 생각하기 때문에
만약 미소유 참조로 선언된 인스턴스가 해제 됐는데 접근하게 되면 런타임 에러가발생합니다.
우선 Customer와 CreditCard 두개의 클래스를 선언합니다.
여기서 Customer는 card 변수로 CreditCard 인스턴스를 참조하고 있고
CreditCard는 customer로 Custome인스턴스를 참조하고 있습니다.
customer는 미소유 참조 unowned로 선언합니다.
이유는 고객과 신용카드를 비교해 봤을때 신용카드는 없더라도 사용자는 남아있을 수 있겠죠?
그래서 CreditCard에 customer를 unowned로 선언합니다.
이제 고객 변수 sojin을 옵셔널 타입으로 선언합니다.
customer은 nil 값이되면 안되므로 옵셔널 타입을 선언하면 안됨
선언한 고객에 인스턴스를 생성하고 고객의 카드변수에도 카드 인스턴스를 생성해 할당해줍니다.
이 시점에서의 참조 상황을 그림으로 표현하면 아래 그림과 같습니다.
sojin이 Customer 인스턴스를 참조하고 있고 CreditCard 인스턴스도 Customer 인스턴스를 참조하고 있지만
미소유(unowned) 참조를 하고 있기 때문에 Customer 인스턴스에 대한 참조 횟수는 1회가 됩니다.
이 상황에서 sojin 변수의 Customer 인스턴스 참조를 끊어봅시다!
그러면 더이상 Customer 인스턴스를 강하게 참조하고 있는 인스턴스가 없으므로 Customer 인스턴가 해제되고
인스턴스가 해제됨에 따라 CreditCard 인스턴스를 참조하고 있는 개체도 사라지므로
CreditCard 인스턴스도 메모리에서 해제됩니다.
3.
Unowned Reference and Implicitly Unwrapped Option Properties
미소유 참조와 암시적 옵셔널 프로퍼티 언래핑
약한 참조, 미소유 참조의 구분을 해당 참조가 nil이 될 수 있느냐 없느냐로 구분할 수 있습니다.
하지만 이 두경우를 제외한 제 3의 경우도 발생할 수 있습니다.
두 프로퍼티가 항상 값을 갖지만 한번 초기화 되면 절대 nil이 되지 않는 경우 입니다.
이 경우에는 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 언래핑을 사용해 참조 문제를 해결할 수 있습니다.
이번에도 코드를 보겠습니다!
Country의 capitalCity는 초기화 단계에서 City 클래스에 초기화 된 후 사용되게 됩니다.
즉 실제로 Country의 capitalCity는 옵셔널이 되어야 맞지만
여기서는 느낌표 연산자(!)를 이용해 명시적으로 강제 언래핑을 시켰습니다.
그래서 암시적 언래핑이 돼서 Country에서 name이 초기화 되는 시점에 self를 사용할 수 있게 됩니다.
또한 City에서는 강한 참조 순환을 피하기 위해
미소유 참조로 country를 선언해서 두 인스턴스를 문제없이 사용할 수 있습니다.
Strong Reference Cycles for Closures
클로저에서의 강한 참조 순환
클로저는 self를 캡쳐하기 때문에 변수 강한 참조 순환이 발생할 수 있습니다.
이 문제를 해결 하기 위해서는 클로저 캡쳐 리스트를 사용합니다.
예제 코드를 보겠습니다.
아래 HTMLElement 클래스의 클로저 asHTML는 입력 값을 받지 않고
반환값이 String인 () -> String 클로저를 사용합니다.
그리고 이 클로저 안에서 self.text와 self.name과 같이 self를 캡쳐하게 됩니다.
asHTML 클로저는 아래와 같이 다른 클로저로 변경 될 수도 있습니다.
아래 코드를 실행하면 결과는 다음과 같습니다.
위에서 paragraph 변수는 HTMLElement?로 선언됐습니다.
그래서 변수에 nil을 할당할 수 있지만 아래 그림과 같이 강한 참조 순환에 빠지게 됩니다.
아래와 같이 인스턴스와 클로저 간에 강한 참조를 하게되어 강한 순한 참조에 빠지게 됩니다.
아래와 같이 paragraph의 참조를 nil로 할당하더라도 HTMLElement인스턴스는 해제되지 않습니다.
클로저 안에서 self를 여러번 참조하더라도 실제로는 단 한번의 강한 참조만 캡쳐합니다.
Resolving Strong Reference Cycles for Closures
클로저에서 강한 참조 순환 문제의 해결
클로저에서 강한 참조 순환 문제의 해결하기 위해 캡쳐 참조에 강한 참조 대신
약한 참조(weak) 혹은 미소유(unowend) 참조를 지정할 수 있습니다.
약한 참조인지 미소유 참조를 사용할지는 코드에서 상호 관계에 달려있습니다.
Swift에서는 클로저에서 특정 self의 메소드를 사용할 때 캡쳐를 실수하는 것을 막기위해 someProperty 혹은 someMethod 대신 self.someProperty 혹은 self.someMethod와 같이 self를 명시하는 것을 필요로 합니다.
1.
Defining a Capture List
캡쳐리스트 정의
캡처리스트를 정의하기 위해서는 클로저의 파라미터 앞에 소괄호([])를 넣고
그 안에 각 갭쳐 대상에 대한 참조 타입을 적어 줍니다.
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
클로저의 파라미터가 없고 반환 값이 추론에 의해 생략 가능한 경우에는 캡처리스트 정의를 in앞에 적어 줍니다.
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
2.
Weak and Unowned References
약한 참조와 미소유 참조
앞서 인스턴스 참조와 마찬가지로
참조가 먼저 해제되는 경우는 약한 참조를
같은 시점이나 나중 시점에 해제되는 경우에는 미소유 참조를 사용합니다
만약 캡쳐리스트가 절대 nil이 될 수 없다면 그것은 반드시 약한 참조 리스트가 아니라 미소유 참조 리스트로 캡쳐돼야 합니다.
자 이제 클로저에 적절한 캡쳐 리스트를 적어 코드를 실행해 보도록 하겠습니다
asHTML 클로저의 self에 [unowned self]라고 캡쳐리스트를 아래 코드와 같이 적어줍니다.
앞서와 같이 인스턴스를 생성해 실행합니다.
참조 상황을 그림으로 살펴보면 앞서와는 다르게 클로저에서 HTMLElement 인스턴스를 미소유 참조로 참조하고 있습니다.
그래서 paragraph의 참조를 제거 하면 HTMLElement 인스턴스가 바로 메모리에서 해제되는 것을 확인할 수 있습니다.
정리
weak reference: 참조하고 있는 인스턴스가 먼저 메모리에서 해제될때 사용
unowned reference: 참조하고 있는 인스턴스가 같은 시점 혹은 더 뒤에 해제될때 사용
'Swift' 카테고리의 다른 글
[Swift] Method 랑 Computed Property 중 어떤걸 사용해야할까? (0) | 2023.01.29 |
---|---|
[Swift] 생성자(initializer) (0) | 2023.01.29 |
[Swift] Method (메소드) (0) | 2023.01.29 |
[Swift] Diffable Datasource와 Compositional Layout (1) | 2023.01.29 |
[Swift] ARC(Automatic Reference Counting) (0) | 2023.01.29 |