WWDC/Swift

[Swift] WWDC21 - ARC in Swift : Basics and beyond

유정주 2022. 10. 21. 11:35
반응형

서론

지난 포스팅에서 CPU, 메모리같은 앱의 한정된 자원을 절약해야 한다고 말했었습니다.

그럼 메모리에 관한 공부가 더 필요하겠다고 생각해서 ARC에 관한 WWDC 영상도 찾아보았습니다.

익숙한 줄 알았던 ARC에 대해 더 자세히 알게 된 경험이었습니다. (WWDC 꿀잼)

 

(

좀 놀랐던건 전 지금까지 ARC를 "에이알씨"라고 읽었는데 "아크"라고 읽더라구요...

다른 분들도 아크라고 읽으시나요? 저만 에이알씨라고 읽었나요? ㅋㅋ ㅠㅠ

댓글로 알려주세요...

)

 

ARC in Swift : Basics and beyond

이번 세션에서는 ARC가 어떻게 작동하는지 알고, 객체 수명에 대한 내용을 다룹니다.

이를 제대로 알아야 효과적인 Swift 개발이 가능합니다.

 

Object lifetimes and ARC

Swift의 객체 수명은 init으로 시작해서 마지막 사용 후 끝납니다.

ARC는 수명이 끝난 객체를 할당 해제해서 자동으로 메모리를 관리합니다.

ARC는 Swift 컴파일러가 자동으로 삽입하는 retain과 release 코드로 동작하는데요.

런타임 시, retain은 Reference Count(이하 RC)를 증가시키고, release는 RC를 감소 시킵니다.

release에 의해 RC가 0이 되면 객체가 할당 해제됩니다.

 

Traveler로 예를 들어보겠습니다.

Traveler 클래스는 이름(name)과 목적지(destination) 프로퍼티를 가지는 클래스입니다.

 

컴파일 타임 흐름을 먼저 살펴봅시다.

 

test 함수에서는 traveler1 객체를 만듭니다.

이때 traveler1은 RC가 1인 상태로 생성됩니다.

컴파일러가 삽입한 retain에 의해 RC가 0에서 1로 증가하는 것이 아니었네요.

traveler1은 traveler2에 복사가 되면 더이상 쓰이는 곳이 없어집니다.

컴파일러는 이를 알고 복사 코드 이후에 release 코드를 삽입합니다.

 

traveler2를 보겠습니다.

traveler2는 복사가 될 때부터 destination을 변경할 때까지 참조가 유지됩니다.

이때는 retain에 의해 RC가 1 증가합니다.

객체 생성이 아닌 참조 복사에 의해 객체가 시작된 것이기 때문입니다.

traverler2의 쓰임이 다하면 release를 삽입하여 RC를 0으로 감소시킵니다.

 

컴파일에 삽입된 retain과 release 코드가 런타임에는 어떻게 동작하는지 볼까요?

traveler1 객체가 init에 의해 RC가 1인 채로 Heap에 할당됩니다.

traveler2의 retain에 의해 Traveler 객체의 RC가 2가 됩니다.

그리고 traveler1이 traveler2에 복사되면서 같은 객체를 참조합니다.

더이상 쓸모가 없는 traveler1을 release 하면서 RC가 1 감소하고, traveler1의 참조가 사라집니다.

traveler2가 destination을 수정하면서 nil이었던 메모리 공간에 Big Sur 값이 들어갑니다.

traveler2의 수명이 끝날 때 release가 되면서 RC가 0이 되고, traveler2의 참조도 사라집니다.

RC가 0이 된 Traveler 객체는 메모리에서 해제됩니다.

 

Swift의 객체 수명은 use-based 입니다.

사용이 될 때 객체 수명이 시작해서, 쓸모가 없어지면 수명이 끝납니다.

객체 수명이 끝나면 바로 메모리 할당 해제가 될 거 같지만, ARC 최적화에 의해 메모리 할당 해제 되는 타이밍은 달라질 수 있습니다.

위 예시의 경우

함수 종료 직전에 객체가 메모리 할당 해제가 됩니다.

 

Swift는 왜 이렇게 메모리 관리를 할까요??

 

Ovservable object lifetimes

weak나 unowned 참조를 사용하거나 Deinitializer side-effect를 사용하면 객체의 라이프타임을 관찰할 수 있습니다.

하지만 관측된 객체의 라이프타임에만 의존하면 버그가 생길 수 있습니다.

weak나 unowned 대신 다르게 구현하면 관측된 객체의 라이프타임이 달라지니까요.

한동안 어찌저찌 잘 동작할 수 있지만, ARC 방식이 변경되면 문제가 발생할 수도 있습니다.

(이런 버그를 잡으라고 하면 난죽택할 수도 있을 거에요...)

 

이 내용에 대해 더 자세히 알아보고 이를 수정하는 안전한 기술을 살펴보겠습니다.

 

Reference Cycle

 

weak와 unowned는 Reference Counting에 관여하지 않습니다.

그래서 이 참조들은 Reference Cycle을 끊는데 사용되죠.

그럼 Reference Cycle이란 뭘까요?

 

아래 사진은 Traveler 코드에 printSummary와 Account를 추가한 코드입니다.

Traveler 클래스는 Account 옵셔널 타입을  가지고 있고, account가 nil이 아니면 가지고 있는 포인트를 출력할 수 있습니다.

이때, Traveler는 account 프로퍼티를 통해 Account 타입을 참조하고, Account 타입은 traveler 프로퍼티를 통해 Traveler 타입을 참조합니다.

서로가 서로를 참조하고 있는 상황이죠.

 

test 함수를 이용해서 이런 상황에 ARC는 어떻게 동작하는지 알아봅시다.

test 함수에서는 Traveler 객체에 Account 객체를 할당하고 printSummary 메서드를 실행합니다.

 

Heap에서 발생하는 일을 그림으로 살펴볼게요.

Traveler 객체가 생성되면서 RC가 1이 됩니다.

 

Account가 생성되면서 traveler를 참조하고, Traveler의 RC는 2가 됩니다.

 

그다음, traveler의 account에 Account 인스턴스를 할당하면서 Traveler가 Account를 참조합니다.

Account의 RC도 2가 되면서 서로가 서로를 참조하는 그림이 되었습니다.

 

여기까지 수행이 되면 account 객체는 더이상 쓸모가 없어지기 때문에 release가 됩니다.

그래서 account의 참조는 사라지고 Account 객체의 RC는 1 감소하죠.

근데 2 -> 1로 감소가 되어서 0이 아니기 때문에 메모리에서 해제되지는 않습니다.

 

마지막 코드는 traveler.printSummary()입니다.

메서드가 실행하고 traveler는 더이상 쓸모가 없어 release 됩니다.

release에 의해 RC가 2 -> 1로 감소하지만, 마찬가지로 0이 아니기 때문에 메모리에서 해제되지는 않습니다.

 

객체에 대한 모든 참조가 사라졌지만 메모리에는 여전히 남아있게 됩니다.

바로 Reference Cycle 때문에요!

이 상태에서는 영원히 메모리 해제가 되지 않고, memoery leak을 발생시킵니다.

 

weak와 unowned

weak와 unowned 참조를 사용하면 Reference Cycle을 깰 수 있습니다.

Reference Counting에 참여하지 않기 때문에 위와 같은 상황에 메모리 해제가 될 수 있기 때문입니다.

이 경우, Swift는 weak일 때는 nil을, unowned일 때는 trap을 할당하여 안전하게 전환합니다.

 

위 예시의 Account의 traveler 프로퍼티는 weak로 선언했다고 합시다.

 

이제 Account가 traveler를 참조해도 Traveler의 RC는 증가하지 않습니다.

그럼 마지막 그림은 이렇게 변할 것이고, Traveler의 RC는 0이 되어서 메모리 해제가 될 수 있습니다.

Traveler가 메모리 해제되면 Account의 RC도 0이 되면서 Account도 메모리 해제가 됩니다.

 

weak를 Reference Cycle을 깰 수 있었지만, 위에서 언급했듯 이러한 방식은 버그를 발생할 수 있습니다.

Account 클래스를 조금 수정한 아래 코드를 봐볼까요?

이 코드에서는 printSummary가 Traveler가 아니라 Account에 있습니다.

 

account.printSummary()가 수행되기 전에 traveler가 메모리 해제되서 nil이 된다면 account는 더이상 traveler 인스턴스에 접근할 수 없습니다.

그럼 printSummary에서는 런타임 에러가 발생할 것입니다.

옵셔널 바인딩을 하면 되지 않아? 라고 할 수 있지만,

그럼 printSummary()를 호출해도 아무것도 출력이 되지 않아 원하는대로 동작하지 않을 것입니다.

(이를 Silent Bug라고 표현하네요.)

 

Consequences and safe techniques

weak와 unowned가 적용된 인스턴스를 더 안전하게 사용할 수 있는 방법이 있습니다.

withExtendedLifetime 함수, 강한 참조로 Redesign, weak/unowned를 피하는 방법입니다.

하나씩 살펴보겠습니다.

 

withExtendedLifetime

withExtendedLifetime은 원하는 메서드나 프로퍼티를 사용할 때까지 객체의 라이프타임을 연장할 수 있습니다.

이렇게 하면 printSummary가 호출되는 동안 traveler의 라이프타임이 연장되서 버그를 방지할 수 있습니다.

이렇게 호출 순서를 변경해도 동일한 효과를 낼 수 있습니다.

만약 더 복잡한 로직이라면 defer를 이용해 현재 scope(코드 블럭)이 끝날 때까지 라이프타임을 연장할 수도 있어요.

 

withExtendedLifetime()을 이용한 방법은 손쉽게 객체의 라이프타임 버그를 해결할 수 있어 보이지만,

이는 불안정하고 개발자에게 책임을 미루는 방식입니다.

weak를 사용할 때마다 withExtendedLifetime를 써야하니 번거롭고 위험하다는거죠.

 

Redesign to access via strong reference

두 번째 방법은 강한 참조로 클래스를 재설계하는 것입니다.

printSummary 메서드를 다시 Traveler로 옮기면 account에 다시 접근할 수 있게 됩니다.

Account의 traveler는 private로 바꿔서 혹여나 다른 곳에서 weak 프로퍼티에 접근하는 것을 막습니다.

 

이 방법도 개발자에게 책임을 미루는 방법이라 근본적인 해결이 필요합니다.

 

Redesign to avoid weak/unowned reference

weak/unowned를 쓰기 전에 이게 정말 필요할지 생각해봐야 합니다.

아예 사용하지 않는 방향으로 재설계를 하는 거죠.

 

위 예시는 Traveler 클래스만 Account 클래스를 참조하기만 하면 됩니다.

Account가 굳이 Traveler를 참조할 필요가 없죠.

 

이 점에 주목해서 클래스를 재설계 해보면

name을 가진 PersonalInfo 클래스를 바라보면서, Reference Cycle이 사라졌습니다.

이 방법처럼 재설계하는 것은 구현 비용을 증가시킬 수 있지만, 객체의 라이프타임으로 인한 모든 버그를 제거할 수 있습니다.

 

Deinitializer side-effects

객체의 라이프타임을 관찰할 수 있는 또다른 방법은 Deinitializer size-effect가 있습니다.

 

Deinitializer

Deinitializer는 객체가 메모리에서 해제되기 직전에 실행됩니다.

Deinitializer는 ARC 최적화 상황에 따라 호출 순서가 달라질 수 있다는 문제가 있습니다.

현재의 ARC 버전에서는 deinit이 "Done traveling" 이후에 호출되지만, 미래의 언젠가 ARC 최적화가 변경되어 print가 된 후 호출될 수도 있습니다.

 

이 예시에서는 print만 하는 것이니 큰 문제가 아닐 수 있지만 deinit에 중요한 로직이 있다면 문제가 커질 수 있습니다.

이 상황에서도 withExtendedLifetime(), 클래스 재설계, deinit 대신 defer 사용을 통해 문제를 해결할 수 있습니다.

 

Xcode13 Optimize Object Lifetiems

Xcode 13부터 Build Setting에서 Optimize Object Lifetimes를 설정할 수 있습니다.

YES로 설정하면 라이프타임이 짧아지게 되서 위에서 언급한 현상을 볼 수 있습니다.

참고해서 디버깅을 하면 좋을 것 같네요.


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형