[Swift] weak & unowned

Uno·2021년 8월 17일
1

Tip-Swift

목록 보기
16/26

swift에서 “ARC” 혹은 “강한참조의 우려 때문에…” 이런 말로 weak, unowned 등을 사용하는 코드를 본 경험이 있으실겁니다. 있다고 하자

해당 부분을 간략하게 정리해보고자합니다.

왜 사용하는 걸까?


모야 :weak는 왜 사용하는 건가요?
우노 :
WWDC에서 weak reference (약한 참조) 는 Strong Reference Cycle (강한 순환 참조) 를 벗어나기 위해서 사용한다고 언급합니다.

Strong Reference Cycle은 memory leak 의 원인이 됩니다. 그래서 벗어나려고 하는 거죠. 상황을 간단히 묘사해볼게요.

1. A가 B를 참조한다.
2. B가 A를 참조한다.
(서로가 서로를 무한히 참조하는 경우죠? 마치 거울을 두 개 맞닿아 놓은 상황입니다.)
3. A가 메모리에서 해제되려고 한다.
4. 그런데 B가 참조하고 있다. 그리고 A도 B를 참조하고 있다.
5. iOS는 현재 두 객체는 메모리에서 해제하면 안된다고 판단한다.
(그렇게 둘은 앱이 종료될 때까지 유지된다.)

A가 사라질 때, “B가 A를 참조” 이 사실이 서로를 붙잡고 있는 겁니다.

그러면

B가 A를 참조하긴 하는데, “약한” 참조로 A가 사라지면 참조를 해제해라!
라고 명령한다면, 문제는 해결되지 않을까요?

그래서 사용하는 개념이 “weak” 입니다.

두 상황을 그림으로 볼게요.
1. 강한참조
-> 그림을 보시면, 서로가 무한히 연결되어 있죠. 끊어질 틈이 없습니다.

  1. 약한참조
    -> 한쪽에 점선이 있죠. 끊어질 틈이 있습니다.

코드로 보자


간단히 코드로 문제 상황을 묘사해봤습니다.

class A {
    var b: Int = 1
    func afterTwoSecPrint() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.b = 99
            print(self.b)
        }
    }
    
    init() { print("메모리에 할당되었습니다.") }
    deinit { print("메모리에서 해제되었습니다.") }
}

// "A" 인스턴스를 생성한다.
var a: A? = A()

// A의 메소드를 호출한다.
a?.afterTwoSecPrint()
// 그러면 b에 1을 할당하고 메소드가 시작된다.
// 2 초 후에 b에 99를 할당하고 print 한다.

// a를 메모리에서 해제한다.
a = nil
print("a에 nil을 할당한 시점")

어렵지 않은 코드죠?

A를 생성 > A의 메소드를 호출 > A에 nil을 할당
입니다. 그러면 2초 후에 실행되는 DisPatchQueue 클로저가 중간에 메모리가 해제되어 동작하지 않길 바랄 겁니다. 하지만 결과는 다음과 같습니다.

<Result>
메모리에 할당되었습니다.
a에 nil을 할당한 시점
99
메모리에서 해제되었습니다.

메모리에 할당되고, nil을 바로 할당했음에도
99까지 호출된 이후에 메모리에서 해제하고 있습니다.

상황을 설명하면 다음과 같습니다.

1. a가 메모리에 할당된다. (reference Count = 1)
2. a에 있는 클로져 블럭이 실행된다. (reference Count = 2)
3. a에 nil을 할당한다. (reference Count = 1)
4. Capturing Value 값이 필요없어진다.
5. a를 메모리에서 해제한다. (reference Count = 0)

이렇게 설명하니 어느 부분을 디버깅하면 될지 보이시나요?

바로 2번 로직 “2. a에 있는 클로져 블럭이 실행된다. (reference Count = 2)” 입니다.

여기서 reference Count를 추가하지 않으면 되겠죠.

그래서 나온 개념이 weak 이구요.

코드를 수정해보겠습니다.
코드는 DispatchQueue 클로저에 [weak self] 만 추가했습니다.

import UIKit

class A {
    var b: Int = 1
    
    func afterTwoSecPrint() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.b = 99
            print(self?.b)
        }
    }
    
    init() {
        print("메모리에 할당되었습니다.")
    }
    
    deinit {
        print("메모리에서 해제되었습니다.")
    }
}

// "A" 인스턴스를 생성한다.
var a: A? = A()

// A의 메소드를 호출한다.
a?.afterTwoSecPrint()
// 그러면 b에 1을 할당하고 메소드가 시작된다.
// 2 초 후에 b에 99를 할당하고 print 한다.

// a를 메모리에서 해제한다.
a = nil
print("a에 nil을 할당한 시점")

결과는 다음과 같습니다.

<Result>
메모리에 할당되었습니다.
메모리에서 해제되었습니다.
a에 nil을 할당한 시점
nil

바로 메모리에서 해제되고 있죠.
이렇게 ARC 관리를 통해서 메모리 누수(Memory leak)를 막을 수 있습니다.

모야 : 근데 unowned 라는 걸 사용해도 동일한 효과가 나는 것 같은데요?
코드봐바요!

class A {
    var b: Int = 1
    
    func afterTwoSecPrint() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [unowned self] in
            self.b = 99
            print(self.b)
        }
    }
    
    init() {
        print("메모리에 할당되었습니다.")
    }
    
    deinit {
        print("메모리에서 해제되었습니다.")
    }
}

// "A" 인스턴스를 생성한다.
var a: A? = A()

// A의 메소드를 호출한다.
a?.afterTwoSecPrint()
// 그러면 b에 1을 할당하고 메소드가 시작된다.
// 2 초 후에 b에 99를 할당하고 print 한다.

// a를 메모리에서 해제한다.
a = nil
print("a에 nil을 할당한 시점")
<Result>
메모리에 할당되었습니다.
메모리에서 해제되었습니다.
a에 nil을 할당한 시점

우노 : 둘 의 차이를 한 번 정리해볼까요?

Weak(약한참조)와 Unowned(미소유 참조) 차이


먼저 둘의 특성을 정리해볼게요.

  • Weak
    - 자신이 참조하는 인스턴스의 Reference Count를 증가시키지 않습니다.
    - 옵셔널 타입으로 선언해야합니다.

  • Unowned
    - 자신이 참조하는 인스턴스의 Reference Count를 증가시키지 않습니다.
    - 옵셔널 타입으로 선언해서는 안됩니다. = 인스턴스가 메모리에서 해제되지 않는다.

특성의 차이는 하나죠?

하나는 nil이 될 수 있다. 하나는 nil이 되지 않는다.

그 특성으로 인해 Unowned는 인스턴스가 메모리에서 해제되지 않습니다.
(영원히 안된다는 게 아니라, 참조하는 인스턴스가 해제되기 전까지 해제되지 않는다는 뜻입니다.)
만약 해제한다면, 크래쉬가 발생합니다.

그러니까 Weak는 중간에 참조하고 있는 인스턴스가 메모리에 있든 없든 자기 혼자서 메모리에서 해제되도 됩니다.

그에 비해서 Unowned는 참조하고 있는 인스턴스가 있는 한 메모리에서 해제되어선 안됩니다.

메모리영역에서 설명드리자면, 다음과 같습니다.
weak 참조는 메모리에 해제되면서 스텍 주소값에 nil이 할당되어 약한참조가 구성됩니다.

unowned는 매모리 해제 이후에 스텍 영역에 nil을 저장하지 않고, 댕글링포인터(원래 바라보던 객체가 해제되면 그 공간이 비어있죠. 그 공간을 가르키는 포인터를 의미합니다. 뭔가 “댕글” 하니까 댕청댕청한 느낌? 들죠.) 그러면 크래쉬가 나서 종료되어버리는거죠.

그래서 unowned로 선언한 변수는 반드시, 참조하는 인스턴스보다 오래 살아있을 때, 만 사용합니다.
(마치 강제 언래핑처럼 뭔가 확신할 수 있을때, 간헐적으로 사용하는게 좋겠죠.)

참고자료


profile
iOS & Flutter

0개의 댓글