Swift🐤

Swift) ARC(Automatic Reference Counting)

성실농장주 2023. 9. 1. 19:15

C언어를 사용하다 보면 동적으로 메모리를 할당받았을 때 메모리를 해지를 해야 한다는 부담을 가지게 됩니다.

하지만 JAVA와 같은 경우에는 garbage collector라는 기능을 지원해줘서 이러한 부담을 덜어줍니다.

Swift 같은 경우에도 메모리를 관리해 주는 기능이 있는데 이것이 바로 ARC!입니다.

 

ARC란?

ARC(Automatic Reference Counting)는 이름에서 알 수 있는 참조하는 횟수를 자동적으로 세어서 앱의 메모리 사용을 추척하고 관리하는 것입니다.

ARC 객체를 사용하기 위해 할당받은 메모리를 자동적으로 해지해주기 때문에 덕분에 개발자는 개발을 할 때 메모리 사용에 부담을 덜어줍니다.

(plus: class와 closure는 참조 타입structure(구조체), enum(열거형)은 값 타입입니다.)

 

하지만 ARC를 통해 메모리를 관리하기 위해서는 위해서는 개발자의 코드 정보가 필요합니다.

 

ARC 동작방식

메모리를 관리하기 위해서는 ARC를 사용한다고 했습니다.

 

메모리를 관리 목적

더 이상 사용하지 않는 메모리 영역을 확보해서 다음에 사용될 메모리 영역을 메모리에 할당하기 위함입니다.

만약 그렇지 않는다면 메모리 누수!(더 이상 쓸모없는 데이터들이 메모리 영역에 죽치고 있는것... 민폐..)

 

그런데 

어떻게 메모리에 존재하는 영역이 프로그램 실행에 있어서 필요한지 어떻게 알 수 있을까?

 

이런 구분할 수 능력이 없다면??

객체를 사용중임에도 불구하고 ARC가 해당 메모리를 해지하게 된다면?? -> 대참사.. 치명적..

 

그렇지 않기 위해서는 ARC는 프로퍼티, 상수 그리고 변수가 현재 각각의 클래스 객체를 몇 번 참조하고 있는지 추적해서 이게 쓸모 있는 메모리 영역인지 구분하게 됩니다.

참조의 횟수를 세는 여러가지 방식이 있습니다. 

강한 참조(strong reference)

약한 참조(weak reference)

미소유 참조(unowned reference)

Strong Reference

특징

  • 인스턴스를 변수나 상수에 할당하면 RC가 1이 증가
  • 인스턴스가 할당된 변수나 상수에 nil값을 주면 RC가 1이 감소
  • 아무런 명시를 해주지 않는다면 Strong reference로 지정(default)

이 처럼 강한 참조는 ARC에서 참조횟수를 증감을 함으로써 해당 메모리 영역이 필요한지 알 수 있습니다.

 

Circular Reference

정의

두 개 이상의 객체가 서로를 강하게 참조(strong reference)하는 상황을 의미합니다.

예시

Person과 Apartment라는 두 개의 클래스가 존재합니다.

Person에 대한 설명을 먼저하자면, Person이라는 객체를 생성을 할 때 name 프로퍼티를 입력해주어야 합니다.

 

두 번째 프로퍼티인 apartment는 Apartment라는 클래스의 optional타입으로 타입을 명시해 주었습니다.

optional타입의 경우 초기화 시 default로 nil값을 할당을 받게 됩니다. 그래서 객체의 초기화 단계에서 프로퍼티 값의 입력이 당장 필요하지 않습니다.

 

객체 생성 이후 더 이상 객체 필요 없어지게 된다면, 객체를 해지하게 되는데 deinit 명령어 이하의 코드를 실행하게 됩니다.

deinit 명령어가 실행되었다는 것은 객체가 해지되었다는 신호라고 볼 수 있습니다.

 

Person클래스 Apartment클래스의 객체들을 각각 john, unit4A이라는 변수가 참조하게 합니다.

이때 아무런 참조에 대한 키워드를 명시해주지 않았기 때문에 이때는 강한 참조(strong reference)하게 됩니다.

 

위에 설명한 것을 그림을 나타낸 것입니다. 

빨간 네모 박스에서 볼 수 있듯이 apartment, tenant 프로퍼티는 nil이 할당된 상태입니다.

john객체의 apartment 프로퍼티에 unit4A를 할당해주고, unit4A 객체의 tenant 프로퍼티에 john을 할당해 줍니다.

이렇게 된다면 Person 객체의 apartment 프로퍼티는 Apartment 객체를 강하게 참조하게 될 것이고

Apartment의 객체의 tenant 프로퍼티는 Person 객체를 강하게 참조하게 될 것입니다.

서로의 객체가 서로를 참조하게 됩니다.

 

문제는 여기서 발생하게 됩니다!

만약 두 객체가 서로를 참조한다는 것을 까먹고

이제 더 이상 Person 객체와 Apartment의 객체가 필요 없어졌다고 생각하고 두 객체를 해지하기 위해 nil을 할당해도 메모리 해지는 발생하지 않게 됩니다.

그 이유는 객체 안에 프로퍼티들이 서로를 참조하고 있기 때문입니다.

ARC 입장에서는 참조의 기록이 남아 있기 때문에 메모리 해지를 못하는 상황이 되게 됩니다.

 

그렇게 된다면, 현재 입장에서 두 객체에 접근할 수 있는 변수가 남아있지 않기 때문에  이후에는 프로퍼티에 nil을 할당하지 못하는 상황이 되게 됩니다.

물론! 변수들에 nil을 할당하기 전에 객체들을 참조하고 있는 프로퍼티에 nil을 할당하게 된다면, 순환 참조는 없어지게 됩니다.

 

하지만! 코드를 짜다보면 순환참조가 발생했음에도 불구하고 인지하지 못하고 객체를 참조하는 변수에 nil을 할당하게 된다면 순환 참조된 메모리 해지를 못하게 되는 상황이 되고

 

결국! 메모리 누수가 발생하게 됩니다.

 

이러한 문제를 방지하기 위해 weak reference와 unowned reference가 존재합니다.

 

Weak Reference

weak reference(약한 참조)는 변수나 프로퍼티 앞에 weak 키워드를 위치 시킴으로써 약한 참조할 수 있습니다.

 

약한 참조는 메모리 해지하는 것에 대해 보호하지 않습니다. 이 말은 약한 참조하고 있는 객체는 메모리 해지될 수 있다는 의미입니다.

구체적인 설명은 아래 예시에서 확인할 수 있습니다.

 

Note: ARC가 weak reference를 nil로 바꾸더라도 프로퍼티 옵저버는 호출되지 않습니다.

 

위에 순환 참조에서 설명을 위해 사용했던 클래스들 입니다. 다만 여기서 차이점은 Apartment의 클래스의 tenant 프로퍼티가 Person 객체를 약한 참조 선언했습니다. 

john, unit4A 변수들은 Person 객체와 Apartment 객체에 각각 강한 참조가 되었습니다.

 

Person 객체는 전에 예시와 같이 Apartment 객체를 강하게 참조하고 전에 예시와 다르게 Apartment 객체는 Person 객체를 약하게 참조하고 있습니다.

 

john 변수에 nil을 할당하는 코드입니다. 그렇게 된다면, Person 객체는 약한 참조만 남게 됩니다.

Person 객체는 더 이상의 강한 참조가 남지 않게 되고 메모리 해지가 되게 됩니다.

그리고 Person 객체를 약하게 참조하고 있던 tenant 프로퍼티는 nil을 할당받게 됩니다.

 

이것이 "약한 참조는 메모리 해지하는 것에 대해 보호하지 않는다"의 예시입니다.

다르게 말하자면, ARC에서 메모리를 관리하는 방법은 참조 횟수에 따라 할당된 메모리 영역을 유지할지 말지가 결정되는데 약한 참조는 참조 횟수에 영향을 주지 않는다고 말할 수 있습니다.

나머지 객체를 메모리 해지를 위해 unit4A에 nil을 할당함으로써 모든 객체를 메모리 해지할 수 있게 됩니다.

 

이 처럼 약한 참조는 강한 참조 순환을 방지하기 위해 사용될 수 있습니다.

 

Unowned Reference

약한 참조와 같이 unowned reference(미소유 참조)도 객체를 강하게 참조하지 않습니다.

그럼으로써, 순환 참조가 발생하지 않게 됩니다.

 

약한 참조와 같이 "unowned"라는 키워드를 변수나 프로퍼티 앞에 위치함으로써 선언할 수 있습니다.

 

약한 참조와 다르게, 미소유 참조는 항상 값을 가지길 요구됩니다. = optional 타입 X

그리고 ARC는 절대 미소유 참조 값을 nil로 설정하지 않습니다.(이러한 이유 때문에 런타임 에러가 발생)

 

밑에 예시를 보면 어떤 상황에서 미소유 참조를 사용해야하는지 명확하게 알 수 있습니다.

설명 위해, 은행 고객을 의미하는  Customer 클래스, 은행에서 발행하는 카드를 의미하는 CreditCard 클래스를 준비했습니다.

두 클래스 서로의 객체를 저장할 수 있는 프로퍼티를 가지고 있습니다.

현실적으로 객체 간의 관계를  생각해보았을때,

보편적으로 은행에서 "손님"의 데이터가 사라지기 전에 "카드"의 데이터가 먼저 없어지게 됩니다.

CreditCard 객체가 메모리 해지되기 전에는 Customer 객체가 항상 메모리 상에 항상 존재한다는 것을 의미합니다.

 

이것을 표현하기 위해 Customer의 card 프로퍼티는 CreditCard를 강하게 참조하고 CreditCard의 customer 프로퍼티는 미소유 참조로 Customer 객체를 참조했습니다.

 

게다가 새로운 CreditCard 객체는 number을 전달 받고 Customer 객체를 전달 받음으로써 생성될 수 있습니다. 이것은 CreditCard 객체가 항상 Customer 객체와 연결된다는 것을 확실하게 해줍니다.

 

이렇게 미소유 참조를 사용함으로써 Cumtomer 객체와 CreditCard 객체간의 강한 참조 순환을 피할 수 있게 됩니다.

 

john이라는 변수에 Cusotomer 객체를 할당해주고, Customer 객체의 card 프로퍼티에 CreditCard 객체를 할당해 줍니다.

 

 

중요!

미소유 참조는 항상 메모리 해지가 되지 않을 객체를 참조한다는 것이 확실할때만 사용하십시오!

만약 너가 그 인스턴스가 메모리 해지가 된 후에 미소유 참조의 값에 접근을 시도한다면, 런타임 에러가 발생하게 될 것입니다.

 

미소유 참조를 한 객체가 사라지고 해당 객체를 참조한 프로퍼티에 접근 했을때의 에러 내용

error: Execution was interrupted, reason: signal SIGABRT.

The process has been left at the point where it was interrupted, use "thread return x" to return to the state before expression evaluation.

 

약한 참조(weak reference)와 미소유 참조(unowned reference)의 공통점?

약한 참조와 미소유 참조는 객체의 참조 횟수에 영향을 주지 않기 때문에 두 방법으로 참조하고 있더라도 객체는 메모리 해지될 수 있습니다.

약한 참조(weak reference)와 미소유 참조(unowned reference)의 차이?

약한 참조의 대상이 되는 객체 사라지고 약한 참조를 진행한 프로퍼티에는 nil 할당됩니다.

약한 참조를 진행하는 프로퍼티는 항상 optional 타입을 가져야합니다.

 

미소유 참조 같은 경우 똑같이 객체가 사라지고 참조를 진행한 프로퍼티를 접근하려하면 런타임 에러가 발생하게됩니다.

미소유 참조를 진행한 프로퍼티는 optional 타입을 갖지 않아도 됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

reference: 

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting

 

Documentation

 

docs.swift.org