안녕하세요
오늘은 ARC에 관하여 설명하고자 합니다
(TMI지만 앞쪽 노래 두둠칫 느낌 좋음)
우선 Swift는 값타입(구조체 / 열거형)을 제공하고 있습니다
value type
struct
enum
참조 타입과 함께 의도치않게 함께 제공되는 경우의 위험을 방지하려는 경우
값 타입을 사용하는 것이 좋습니다
클래스는 참조 타입이고
Reference Type
class
(closures)
ARC
(Automatic Reference Counting)
통하여 메모리를 관리합니다
Swift에서 object lifetimes / ARC에 대해서 알아봅시다
스위프트에서는
객체의 수명은
초기화 시에 시작(init)하여 마지막 사용 시에 종료가 됩니다
ARC는 수명이 끝난 후 객체 할당을 해제(deinit)함으로써 메모리를 자동으로 관리합니다
references count를 추적하면서 객체수명을 결정하고 있습니다
ARC의 유지와 해제는 주로 스위프트 컴파일러에 의해 미리 구동이 되고 있습니다
그리고 런타임에서는 참조 카운트가 증가하고
릴리즈하면 감소하게 되는 형식입니다
그리고 참조 카운트가 0이 되면 해제되는걸 볼 수 있습니다
정리해서 말하자면
retain : 참조 카운트 증가 (+)
release : 참조 카운트 감소
참조 카운트가 0이 되면 객체 해제가 됩니다
우선 들어가기 전에 다른분께 ARC를 물어봤더니
ARC 이전에 MRC가 있었다고 말해서
MRC 메모리 관리에 대해서도 조금 찾아보았습니다
MRC
|
Manual Reference Counting
원스 어폰 어 타임.. retain, release, autorelease를 통해 수동
메모리관리를 했었습니다
정말 과거 개발자들은.. 대단히 신경쓸게 많았을것 같네요
과거에 비해서 지금은 신경쓸게 많이 없다고 느낄 정도..
무튼 이제 간단히 MRC에 관하여 설명하였고
이제 ARC에 관한건
코드를 통해서 알아보고자 합니다
Travler 클래스와
함수 내부에서 Traveler를 인스턴스화하여 만든
travler1 / travleer2를 볼 수 있습니다
여기서 test함수 안에서 traveler1이라는 변수에 Traveler 객체의 인스턴스를 만들어 할당하고
traveler2에 travler1의 값을 복사합니다
그리고 traveler2 속성에 접근하여 destination에서 새로운 값을 할당해주고 있습니다
traveler1 reference가 마지막에 사용되고 나서 컴파일러가 release를 붙여줍니다
왜 카운트가 0으로 나타나지도 않는것 같은데
ENDS???
사용한다는 관점에서 보았을 때는
traveler1은 사용 종료라고 볼 수 있어요
사용 완료가 되었기 때문에 종료가 된거랍니다
컴파일러에서
traveler1 참조에 마지막 사용 후에 즉시
release(해제) operation
하는걸 볼 수 있습니다
retain 키워드가 없는 이유는
생성자는 reference count를 1로 설정해서
컴파일러가 코드에서 retain을 release 처럼 넣어줄 필요는
없답니다 :)
traveler1에 이어서 traveler2에 대해서 알아봅시다
traveler2 객체는 생성자가 아닌 할당을 통한 새로운 참조를 만들어 내는 순간 참조 카운트가 시작되고
traveler의 속성인 destination 값을 업데이트하고난 후
참조가 끝나게 됩니다
컴파일러는 어떻게 retain과 release를 삽입할까?
위에서 다시 말했다 시피 traveler2는 객체가 생성자로 시작하는게 아닌
traveler1을 받아오는 거기 때문에
컴파일러가 retain 키워드를 추가하는걸 볼 수 있습니다
다음으로 런타임에서는 어떤 일이 벌어지는지
살펴보도록 하겠습니다
컴파일러가 모든걸 계산해주는것이 아닌
런타임에서도 일어나고 있습니다
메모리의 구조는 총 4개로 이루어져 있습니다
code / data / heap / stack
Travler object는 heap에서 생성이 됩니다
Traveler는 class 이기 때문에
즉 reference type(참조 타입) 이라 heap에 저장되게 됩니다
그리곤 reference count 가 1인 상태로 init 됩니다
그리곤 다음 라인에 있는 retain 코드를 만나게되면
reference count가 증가하여 1 👉🏻 2로 증가하게 됩니다
traveler2도 동일한 주소값을 가리키는걸 볼 수 있습니다
그리곤
traveler1의 사용이 끝났죠?
끝났기 때문에 release operation이 실행되며
reference count는 1이 됩니다
destination 속성이 "Big Sur"로 업데이트 됩니다
traveler2도 reference의 사용이 끝났기 때문에
release가 실행이 됩니다
그래서 reference count는 0이 됩니다
reference count가 0이 되어야 object를
deallocated(할당 해제)를 할 수 있습니다
스위프트의 object lifetiem(오브젝트 라이프타임)은 사용되는 것을 기반(Use-based)으로 자동 판단을하고
초기화(initialization)에서 시작이 되고 마지막 사용에서
자동적으로 끝이 나게됩니다
스위프트 언어는 c++의 언어와는 다릅니다
c++은 할당 비할당(해제: release)을 직접적으로 해야하기 때문에
자동적으로 카운팅되어서 할당과 할당해제를 해주는 것과는 반대되기 때문이죠!
여기서 보면 object가 마지막 사용 후에
즉각적으로 deallocated(해제) 되는 것을 볼 수 있습니다
하지만
object lifetime(오브젝트 생명주기)들은
컴파일러에 통해서
참조 카운트와 직결되는
retain / release 의 시점으로
생성과 해제가 결정이됩니다
그래서 함수가 종료되지 않아도 객체가 해제될 수 있답니다
반면에
ARC 최적화에 따라서 lifetime이 조금씩 변경이 될 수 있습니다
object를 마지막에 사용한 이후로도 바로 해제되지 않고 생명주기가 좀 더 길어질 수도 있답니다
즉시 해제되지 않고 텀이 조금 걸리는게 보이죠??
정확한 lifetime이 중점적으로 문제를 발생시키지 않지만
이슈가 되는 경우가 있습니다
weak / unowned를 사용하거나 deinit(deinitializer)를 사용하면 문제가 생길 수 있답니다
생명주기를 추적하기 때문에 언제 해제되는지가 중요하답니다
이런 의존적인 코드들은 버그가 일어날 가능성이 농후하기 떄문에
실행이 잘 되더라도 100% 확실히 버그없이 동작하는게 아니랍니다
컴파일러가 변경되거나 해당 코드와 관련이 없는 코드를 변경했을 땐
오랜시간동안 발견되지 않던 버그가 발견된다면
그땐 해결해야겠죠?
처음 만들었던 Traveler 클래스에서
account 클래스와 메서드가 추가된것을 볼 수 있습니다
그리고 Account 클래스에서는
traveler 클래스와 points 속성을 가지고 있습니다
이 코드를 통해서 Traveler / Account 둘 다 서로를 참조하고 있습니다
test함수에서
두 클래스를 인스턴스화 하여
ARC의 변화를 살펴보고자 합니다
Traveler object는 heap에 생성이 되고
reference count 는 1이 됩니다
그리곤 Account를 인스턴스한 순간
Account의 reference count는 1이 되며
Account 내에서 traveler를 참조하고 있기에
Traveler의 reference count가 1에서 2로 올라간것을 볼 수 있습니다
이 코드를 통해서
travler가 account를 참조하기 때문에
account의 reference count는 1에서 2로 증가하게 됩니다
이 코드 줄에서 마지막 account의 reference 사용으로
account의 reference count가 2에서 1로 감소하는걸 볼 수 있습니다
그리곤 traveler 함수가 호출되고 난 후
traveler의 reference count가 2에서 1로 감소하는걸 볼 수 있습니다
테스트 함수 내에서
object가 사용을 끝나고 나서도 reference count가 1로 남게되는 상황이 발생하게 됩니다
reference cycle이 발생하는 이유랍니다
그래서 object가 절대 해제(deallocated)가 발생할 수 없게되며
메모리 누수가 발생하게 됩니다
(영영 쓰일 일이 없는데 메모리를 할당하고 있기 때문이죠)
이러한 상황을 해결하기 위해서는
weak / unowned를 사용할 수 있답니다
reference count에 관여하고 있지 않습니다
weak
선언된 객체에 참조가 되었다면 nil을 반환할 수 있습니다
unowned
trap(덫)을 만들어서 런타임에러를 발생시킵니다
Account 클래스 내에 있는 travler프로퍼티를 weak 키워드를 추가해줍니다
weak을 사용하게 되면
reference counting이 되지 않기 때문에
traveler의 카운트는 0이 됩니다
traveler의 reference count가 0이기 때문에
Account에서 nil로 바뀌고
안전하게 deallocated가 되는걸 볼 수 있습니다
그리곤 account가 reference count가 0으로 감소하기 때문에
안전하게 deallocated되는걸 볼 수 있습니다
이제 Account는 Traveler가 사라지면서
카운트가 0이 되고 두 인스턴스가 안전하게 deallocated 되는걸 볼 수 있습니다
그리고 weak의 다른 예제를 살펴보고자 합니다
위와 동일한거 아냐? 할 수 있지만
traveler와 account의 속성과 메서드가 다른걸 볼 수 있습니다
traveler.account = account
이후로 봤을 때
traveler가 마지막으로 사용한 시점이기 때문에
Traveler의 reference count가 0으로 감소되는걸 볼 수 있습니다
Account에서 traveler를 참조가 사라지며 nil을 반환하고
Traveler는 reference count가 0이기 때문에
안전하게 deallocated(해제)가 됩니다
그리곤 마지막 PrintSummary()를 실행하려고 하면 traveler가 nil이라
실행할 수 없기 때문에 오류가 발생하게 됩니다
이렇게 오류가 생기지 않게 하기 위해서는
옵셔널 바인딩을 사용해 오류를 방지할 수 있습니다
값이없더라도 오류가 발생하지 않기에 숨은 버그를 만들 수 있습니다
그래서 traveler객체 lifetime이 변경되었을 때 인지하기가 어려워집니다
이것을 해결하기 위하여 몇 가지 방법이 있습니다
withExtendedLifetime()
강한 참조를 통하여 접근하도록 재설계하는 방법입니다
weak / unowned를 피하도록 재설계하는 방법입니다
예전에도 ARC를 공식문서에서 공부했지만
이건 처음 보는 녀석..
클로저가 반환되기 전에
인스턴스가 파괴되지 않았는지 확인하고 클로저를 실행합니다
특정 코드 블록 동안 명시적으로 연장하는 함수입니다
ARC최적화로 인해 객체가 조기에 해제되는걸 방지할 수 있는 장점이 있습니다
body closure가 완료될 때까지 traveler의 객체 라이프타임을 늘려주기 때문에
잠재적인 버그까지 방지할 수 있습니다
이렇게 아래를 보시면 동일한 방지책을 볼 수 있습니다
마지막에는
defer안에 이렇게 넣어서 사용할 수도 있습니다
좋은 해결책으로 보이나 이러한 방법은 허술하고 불안정하다고 말할 수 있습니다
weak reference가 잠재적 버그를 가지고 있다고 할 때마다
withExtendedLifetime()을 계속~~ 호출을 해야합니다
그리고 특별한 통제가 없다고 한다면
withExtendedLifetime()가 코드베이스 전반에 뿌려질 수 있으니
이것은 유지보수 비용이 증가가 됩니다
그래서
재설계하는게 더 나을 수도 있습니다
이모두 강한 참조라면 예방이 가능한 부분입니다
이 코드에서는 Account의 traveler 프로퍼티를 외부에서 사용할 수 없도록 private 키워드를 사용하였습니다
그리고 printSummary를 다시 traveler내 메서드로 옮겼습니다
그래서 weak / unowned가 필요할까
순환 참조를 피하기 위해서만 사용을 할까?
이렇게
순환구조인 사이클 구조를 트리 구조로 변경하여서 순환참조를 빗겨나갈 수 있습니다
이렇게 기존의 디자인은 이렇게 되어있지만
사실상
Account 클래스에서 Travel 클래스의 personal Info만 필요한 상황입니다
PersonalInfo 클래스를 하나 더 만들게 되면
Traveler 클래스와 Account 클래스가 PersonalInfo를 참조해도
순환참조가 일어나지 않는걸 볼 수 있습니다
weak / unowned의 키워드를 사용하지 않으려면 추가적인 구현 비용이 들 수 있지만
이렇게 디자인 하는것이 잠재적 object lifetime bug를 명확하게 줄일 수 있는 방안 중 하나입니다
그리고 또 하나
객체의 생명주기로 인하여 발생할 수 있는 문제 중 하나는 Deinitializer와 관련된 문제 입니다
Deinitializer은
객체가 해제되기 직전에 호출이 되는 녀석입니다
생명주기로 인하여 발생할 수 있는 버그에 대한 것과
Deinitializer에 관하여 알아보도록 합시다
deinit 키워드가 있는걸 코드로 알 수 있습니다
우리는 print("Done traveling") 다음에
Deinitializer이 호출되어서 나올 수 있지만
어떤경우엔 프린트되기전에 해제가 될 수도 있습니다
지금의 예시는 외부에 영향을 주지 않으니 문제는 없지만
아래 경우는 다를 수 있습니다
Traveler 클래스에서 TravelMetrics의 속성을 참조하며 의존하고 있습니다
Traveler가 해체되면서 TravelMetrics의 publish() 메서드를 호출하고 traveler에 대한 정보를
출력합니다
TravelInterest메서드를 실행하고 난 후
deinit을 실행한다면
위에서 매개변수로 새로운 목적지를 업데이트 했고 그 목적지에 따라서
새로운 카테고리가 생성이 됩니다
하지만
traveler 객체의 사용은 computeTravelInterest()전에 deinit이 호출되면서
publish() 메서드가 실행되게 됩니다
그렇게 computeTravelInterest()가 실행되지 않아 카테고리가 부여되지 않았기 때문에
카테고리에 nil이 들어가는 오류가 발생하게 됩니다
위에서 말했던것 처럼
withExtendedLifetime() 사용할 수 있지만
메모리 관리가 필요하게 됩니다
그래서 항상 적절한 상황에서 사용해야합니다
유지비용 증가 이슈
이렇게 구현을 할 수 있겠죠?
deinitializer에 카테고리를 부여하는 함수를 publish를 넣을 수 있지만
근본적인 해결책은 아닙니다
deinit side effect를 제거하는게 근본적으로 가장 좋기 때문이죠
deinitializer에서 발생할 수 있는 오류를 제거하기 위해서
deinit에서는 상태를 변경하게되는 메서드를 아예 넣지 않는 방법입니다
대신 defer 키워드를 넣어서 함수가 종료되기 직전에 호출할 수 있습니다
XCode13 스위프트 컴파일러 세팅에 Optimize Object Lifetime에 대한 최적화가 추가되었고
이 옵션을 사용하게 되면
객체의 사용이 끝났을 때 메모리에서 해제되는 지점에 빠르게 해제가 될 수 있다고 합니다
그리고 object Lifetime을 일관성 있게 다룰 수 있다는 장점이 있습니다
그렇게 되며 생명주기와 관련된 잠재적인 버그들을 쉽게 발견이 가능하다는 말씀!
무조건 바인딩만 믿고 사용하면 nil로 인하여 잠재적인 버그가 발생할 수 있다는 점을 알게되었고
만약 발생할 수 있다면 XCode13에서 컴파일 최적화로 만들어진걸 사용할 수도 있겠지만
애초에 순환 참조가 발생하지 않는 방식으로 설계를 하도록 해야겠다
그리고 사용이 끝나고 해제를 하는것과 최적화에 따라서
발생하는 시점이 다른것도 알게되어서 유익했습니다
그리고 순환참조가 발생하게되면
무조건 약한참조나 비소유참조를 무턱대고 사용할게 아니라
내가 지금 설계한 코드 자체가 제대로 짠 코드인지에 대해서
한번 더 고민하고 사용하지 않아도 될 키워드를 남용하여
메모리 남용에 이바지 하고있는게 아닌지에 대해서 생각할 수 있는 시간이였습니다
그리고 weak에 대한 예시는 충분히 있었지만
unowned에 대한 것도 알려주셨음 얼마나 좋았을까
(제 욕심이겠죠?..)
❤️혹시나 잘못된 부분이 있다면 댓글로 알려주면 감사하겠습니다❤️
✏️참고
https://developer.apple.com/videos/play/wwdc2021/10216/
'Swift' 카테고리의 다른 글
Swift | weak / unowned에 관하여 (0) | 2024.05.23 |
---|---|
Swift | Error Handling(에러 처리) 와 Result Type에 관하여 (1) | 2024.05.23 |
Swift | 클로저(Closure)에 관하여 -2 (0) | 2024.05.21 |
Swift | 클로저(Closure)에 관하여 -1 (0) | 2024.05.21 |
Swift | self vs Selft에 관하여 (0) | 2024.05.21 |