Notice
Recent Posts
Recent Comments
Link
«   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
Tags more
Archives
Today
Total
관리 메뉴

도니의 iOS 프로그래밍 세상

[Swift] weak, unowned 차이 - ARC 기초편 3탄 본문

Swift

[Swift] weak, unowned 차이 - ARC 기초편 3탄

Donee 2023. 1. 10. 20:00

안녕하세요, 지난 포스팅에선 Strong Reference와 그로 인해 발생하는 Strong Reference Cycle을 해결하는 방법에 대해서 배웠습니다.

그때 순환참조를 해결하기 위해서, 직접 참조를 nil로 지정해 Reference Count를 감소 시키는 방법, 참조를 weak, unowned reference로 함으로써 Reference Count를 증가시키지 않는 방법에 대해서 배웠습니다.

그럼 이번시간에는 weak, unowned는 무엇이며, 이들이 어떻게 Reference Count를 증가시키지 않았는지에 대해 배우겠습니다.

Weak, Unowned 사용법

Weak와 Unowned의 가장 큰 공통점은 특정 객체를 참조할 때 Reference Count를 증가시키지 않는 것입니다.

이 두가지 키워드를 사용하는 방법은 다음과 같습니다

property나 variable을 선언하기 전, weak, unowned 키워드를 앞에 붙여줍니다

weak var example: Example?
unowned var example: Example?

사용방법, Reference Count를 증가시키지 않는다는 공통점이 존재하는 반면, 차이점은 무엇일까요?

Weak Reference

  1. weak Reference는 객체에 강한 참조를 유지하지 않기 때문에, ARC가 참조하는 객체를 해제하려고 할때 막을수가 없습니다. 이로인해 strong reference cycle을 피할수 있게 됩니다
  2. 만약 weak는 자신이 가리키고 있는 instance가 메모리로부터 해제되면 ARC에서 자동적으로 nil을 처리합니다.

밑의 예시를 보도록 하겠습니다.

class Job {
    weak var name: Name?

    deinit {
        print("deinit Job")
    }
}

class Name {
    var job: Job?

    deinit {
        print("deinit Name")
    }

}

var a: Job? = Job()
var b: Name? = Name()

b = nil
a?.name = b
print(a?.name)

위의 프린트문은 어떤게 호출될까요?

weak name이 가르키고 있는 객체가 메모리로부터 해제되었기 때문에, a?.name에는 ARC가 자동적으로 nil을 할당합니다.

  1. weak는 위와같이 런타임때 자동적으로 nil로 값이 변경되는 것을 허락해야 하기 때문에 constant가 아닌, 항상 옵셔널 타입의 변수가 선언되어야 합니다.
    • 만약 let으로 설정했다면, weak는 반드시 mutable variable로 선언되어야 하기 때문에 컴파일 에러가 발생합니다
    • optional을 하지 않아도, 컴파일 에러가 발생합니다.
  2. weak는 다른 옵셔널과 같이 값의 존재를 확인할 수 있고, 유효하지 않은 인스턴스를 확인하더라도 프로그램이 종료되지 않습니다.
    • 이미 메모리에 해제가 되었다라도 nil처리가 되어, 프로그램을 종료시키지 않습니다.
    • 위의 코드에서도, 정상적으로 nil이 프린트 되는 걸 확인할 수 있습니다.
  3. property observer가 nil로 설정될땐 호출되지 않습니다.

Unowned Referecne

unowned reference는 weak reference와 달리, 다른 인스턴스와 수명이 같거나 더욱 긴 라이프타임을 가질때 사용됩니다.

weak와 달리 “항상” 값이 있다는 전제하에 사용됩니다.

그래서 결과적으로 optional을 만들어내지도 않고, ARC또한 자동적으로 unowned reference를 nil로 만들지 않습니다.

그리하여 unowned는 weak와 달리 system을 abort시킬수 있는 위험이 있기 때문에 주의해서 사용해야 합니다.

애플 문서에서도 이를 명확히 권고하고 있습니다

인스턴스가 메모리에서 해제 된 후, unowned reference를 통해서 값을 접근하면 runtime error를 발생한다
그래서 메모리에서 해제 되지 않은 않는게 instance를 참조한다고 확신이 들 때에만 unowned reference를 사용해라

인스턴스가 해제될 때 unowned 변수 값

위의 정의로는, 도대체 이게 어떤 문제를 발생시키는 지 이해가 안갈 수 있으니, 코드로 확인하겠습니다.

class Job {
    unowned(unsafe) var name: Name?

    deinit {
        print("deinit Job")
    }
}

class Name {
    var job: Job?

    deinit {
        print("deinit Name")
    }

}

var a: Job? = Job()
var b: Name? = Name()

a?.name = b
b?.job = a

b?.job = nil
b = nil
print(a?.name)

과연 결과값은 어떻게 출력될까요?

정답은, “컴파일은 가능하나, 실행되고 나서 error로 인해 앱이 종료된다.” 입니다

Unowned의 목적

그렇다면 Unowned는 항상 메모리에 존재하는지 여부도 확인해줘야 하기 때문에 사용하기가 불편한데 Swfit에서는 만든 이유가 멀까요?

Swift문서에서는 아래와 같이 설명합니다. 

 

Swift는 안전하지 않는 unowned reference를 제공함으로써, 성능상의 이유등으로 runtime에 안정성 체크를 disable 시키는 경우를 위해 만들었다.

결론적으로 두가지 이유입니다. 

1. 퍼포먼스

결국, ARC에서 nil처리를 직접 해줘야 하는 사실은 결국 런타임중 해야할 작업이 늘어나는 것입니다.

그리하여 안전하게 사용할 수 있다면, 퍼포먼스면에서 조금 더 우위에 있을수 있는 점이 이유인 것 같습니다.

 

2. 가독성

unowned의 기본적인 전제는 “항상” 값이 있다입니다.

이는 클래스 내부의 값을 가져올때 옵셔널 처리를 하지 않아도 된다는 장점이 있습니다.(순환참조로 인해 Memory leak현상을 하기위해 사용합니다. 자세한 내용은 ARC 4,5탄 포스팅을 참고해주세요.)

 

밑에는 처리를 하지 않았을 때의 예시입니다.

unownedExampleFunction() { [unowned self] value in
	self.someValue = value
	let integerValue = self.someValue
}

weakExampleFunction() { [weak self] value in
	self?.someValue = value
	let integerValue = self?.someValue ?? 0
}

unowned self는 기본적으로, 값이 있음을 근거로 하기 때문에 옵셔널 처리를 하지 않습니다.

하지만 weak의 경우 값이 nil이 될수도 있는 옵셔널 값입니다.

 

따라서 내부 변수를 변경할 땐, 옵셔널 체이닝이 필요하게 됩니다.

또한 내부 변수의 값을 호출할 때에도, 옵셔널 체이닝과 nil-coalescing등 과정을 거치기 때문에 다소 불편한 점이 있습니다.

 

하지만, 우리는 조금의 퍼포먼스 보다는 오래 동작해도 안전한 앱을 지향하기 때문에 weak를 더 많이 사용합니다.

Unowned, Weak로 객체 선언하기

앞서 말한 unowned와 weak의 공통점은 Reference Count를 증가시키지 않는다는 것입니다.

이를 응용한다면, 이들은 특정 객체를 누군가 Strong하게 Instance를 소유하지 않는다면 해당 객체를 가질수 없음을 의미합니다.

일단 코드부터 보시죠!

class Job {
    weak var name: Name?

    deinit {
        print("deinit Job")
    }
}

class Name {
    var number = 5

    deinit {
        print("deinit Name")
    }
}

var a: Job? = Job()
a?.name = Name()
print(a?.name)

이때, 출력값은 어떻게 될까요?

놀랍게도

deinit Name
nil

이 출력됩니다.

앞서 말했듯이, weak와 unowned는 Reference count를 증가시키지 않는 참조입니다.

방금과 같이 객체를 생성하고 , Strong Reference로 누군가가 객체를 소유하고 있지 않으면 이들의 Reference Count는 무엇일까요?

0입니다. 따라서 선언되어도 Reference Count가 0이기 때문에 ARC는 이들을 자동으로 deallocation 시켜줍니다.

 

unowned또한 마찬가지 입니다. 앞에 적은 코드에서 weak을 unowned로 바꾸면 어떻게 될까요?

참조하는 객체가 존재하지 않고, unowned는 “값이 있다”는 전제하에 해당 값을 참조하기 때문에, 프로그램은 에러를 발생시키며 종료됩니다.

이렇듯 unowned, weak는 해당 객체를 가지는게 불가능합니다. 따라서 Strong을 통해서 누군가가 해당 값을 참조하고 있을 경우에만 “유효”한 값을 얻을 수 있습니다.

만약 위 코드를 그대로 작성한다면 Xcode는 친절하게 위와 같은 에러로 알려줍니다.

결론

  1. weak, unowned는 Reference Count를 증가시키지 않는다.
  2. 누군가 Strong Reference로 객체를 가지지 않는 한, weak, unowned는 값을 얻을 수 없다.
  3. weak의 경우 참조 인스턴스가 해제시 ARC가 자동적으로 nil로 바꿔준다.
  4. unowned는 “값이 있다”라는 전제기에, nil로 바뀌지 않는다.
  5. 따라서 해제된 객체를 unowned variable이 호출시 시스템 에러를 발생시킨다.

다음편에서는 ARC의 응용편 클로저, delayed deinitialization에 대해서 배우겠습니다

감사합니다!

참조

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

Comments