Skip to content

Conversation

Hoon94
Copy link
Collaborator

@Hoon94 Hoon94 commented Jan 6, 2025

close #24

19-ARC

1. ARC(Automatic Reference Counting)의 동작 원리는 무엇인가요?

ARC는 Swift에서 객체의 메모리를 관리하는 시스템입니다. ARC는 객체가 더 이상 필요하지 않게 될 때 자동으로 메모리를 해제하여 메모리 누수를 방지하고, 수동으로 메모리를 해제해야 하는 번거로움을 줄여줍니다. ARC의 동작 원리는 객체의 참조 카운트(reference count)를 기반으로 다음과 같은 방식으로 동작합니다.

참조 카운트 관리

- ARC는 객체에 대한 강한 참조(strong reference)를 추적하여 객체가 얼마나 많이 참조되고 있는지 관리합니다.
- 각 객체는 참조될 때마다 참조 카운트를 증가하고, 참조가 해제되면 참조 카운트가 감소합니다.
- 참조 카운트가 0이 되면 객체가 더 이상 필요하지 않다고 판단하여 메모리에서 해제됩니다.

ARC 동작 과정 예시

- 객체가 생성될 때, 참조 카운트는 1로 설정됩니다.
- 다른 객체가 강한 참조를 통해 이를 참조하면 참조 카운트가 증가합니다.
- 강한 참조가 해제되면 참조 카운트가 감소합니다.
- 참조 카운트가 0이 되면, deinit 메서드가 호출되고 객체가 메모리에서 해제됩니다.

이처럼 ARC는 참조 카운트를 관리하며, 객체가 더 이상 필요하지 않게 되면 자동으로 메모리를 해제하여 효율적으로 메모리를 관리합니다. ARC 덕분에 Swift에서 메모리 관리가 간편해졌고, 명시적인 메모리 해제 코드가 필요하지 않게 되었습니다.

ARC와 GC

ARC와 GC 모두 객체의 생명 주기를 관리하기 위한 방법입니다. 생명 주기를 관리한다는 것은 한 객체가 메모리에서 얼마나 살아있는지를 추적하는 것입니다. 이를 추적함으로써 더 이상 필요하지 않은 객체는 메모리에서 해제시킬 수 있습니다.

ARC

ARC란 기존에 수동으로 개발자가 직접 retain/release를 통해 reference counting을 관리해야하는 부분을 자동으로 관리해주는 기술입니다. 컴파일 타임에 컴파일러가 객체의 참조 횟수를 추적하고, 필요에 따라 자동으로 객체를 release하는 코드를 실행 파일에 주입합니다.

장점:

- 컴파일 타임의 작업이며, 컴파일 시점에 언제 참조되고 해제되는지 결정됩니다.
- 객체가 사용되지 않을 때, 실시간으로 메모리에서 release합니다.
- 백그라운드 처리가 없으므로, 모바일 디바이스와 같은 저전력 시스템에 더 효과적입니다.
- 런타임 시점에 추가적인 오버헤드가 발생하지 않습니다.

단점:

- 순환 참조(retain cycle)를 해결할 수 없습니다.
- 즉, 메모리 누수의 위험이 있습니다.

가비지 컬렉션(Garbage Collection)

GC는 런타임에 동작하며, 백그라운드에서 사용되지 않는 객체 및 객체 그래프를 관리합니다.
GC는 불확실한 간격으로 발생하므로, 객체가 더 이상 사용되지 않는 정확한 순간에 반드시 해제 되는 것은 아닙니다.

장점:

- 순환 참조(retain cycle)를 포함하여 전체 객체 그래프를 관리할 수 있습니다.
- 런타임의 작업이며 주기적으로 참조를 추적하여 사용하지 않는 객체를 해제합니다.
- 객체가 해제될 확률이 ARC에 비해 높습니다.

단점:

- 백그라운드에서 수행되므로, 객체의 정확한 release 시간을 알 수 없습니다.
- GC가 발생하는 겨우, application의 다른 스레드가 일시적으로 멈출 수 있습니다.
- 런타임 시점에 객체를 추적하는 과정에서 오버헤드가 발생하며, 이로인한 성능저하가 있을 수 있습니다.

순환 참조에 따른 ARC와 GC의 처리 방식

순환 참조는 두 개(또는 그 이상)의 객체가 서로를 참조할 때 발생합니다. 객체에 대한 외부 참조가 해제 되어도, 서로를 참조하고 있어 객체가 살아있는(alive) 상태를 유지하는 현상을 말합니다.

GC는 reachable 객체를 살펴 보며 동작합니다. 외부 참조가 존재하지 않는 것을 감지하면, 서로를 참조하는 객체 그래프 전체를 버립니다. 따라서 순환 참조 문제가 발생하지 않습니다.

ARC는 더 낮은 수준에서 작동하고, 참조 수를 기반으로 생명 주기를 관리하기 때문에 순환 참조를 자동으로 처리할 수 없으며, 결과적으로 메모리 누수 문제가 발생합니다.

ARC는 순환 참조를 피하는 방법을 제공하지만, 개발자의 명시적인 설계가 필요합니다. 이를 위해 ARC는 strong, weak, unowned와 같은 Storage Modifier를 도입한 것입니다.

런타임에 수행되는 GC는 항상 메모리를 차지하고 감시해야하기 때문에 메모리 사용량이 더 늘어날 수 밖에 없으며, 지속적인 감시를 위해 CPU를 일부 사용해야만합니다. 반면에 ARC는 컴파일러가 메모리 반환 코드를 삽입해주는 것이기에 오버헤드에서 비교적 자유롭다는 특징이 있습니다. 이는 특히 메모리와 CPU가 데스크탑에 비해 제한적인 모바일 기기에서는 더 중요한 문제이고 그만큼 성능 측면에서 이점입니다.

2. Retain Cycle이 발생하지 않도록 방지하는 방법은 무엇인가요?

  • 순환 참조(Retain Cycle)는 두 객체가 서로 강한 참조를 할 경우 발생하며, 참조 카운트가 0이 되지 않으므로 메모리에서 해제되지 않는 문제를 야기합니다.
  • 대부분의 경우, 객체는 강한 참조를 통해 다른 객체를 참조하며, 이때 참조 카운트가 증가합니다.
  • 순환 참조 문제를 방지하기 위해 약한 참조(weak reference)와 비소유 참조(unowned reference)를 사용하여 ARC의 참조 카운트를 조정할 수 있습니다.

약한 참조 (Weak Reference)

약한 참조는 참조하고 있는 객체가 메모리에서 해제되면 자동으로 nil로 설정됩니다. ARC는 약한 참조의 경우 참조 카운트를 증가시키지 않기 때문에 순환 참조를 방지할 수 있습니다.

비소유 참조 (Unowned Reference)

비소유 참조는 참조하고 있는 객체가 항상 메모리에 있다고 가정할 수 있을 때 사용합니다. 즉, 대상 객체의 생명주기가 현재 객체의 생명주기보다 길거나 같다는 가정이 있을 때 사용하며, 약한 참조와 마찬가지로 참조 카운트를 증가시키지 않습니다.

비소유 참조 대상 객체가 존재한다는 가정에서 사용하기 때문에 객체가 해제되었을 때도 nil이 아닌 값을 유지하므로 강제로 해제된 객체를 참조하면 런타임 오류가 발생할 수 있습니다.

사이드 테이블

weak를 사용하는 경우 사이드 테이블을 생성합니다. 즉, 사이드 테이블을 위한 추가 비용이 발생합니다. 하지만 unowned의 경우 사이드 테이블을 사용하지 않습니다.

약한 참조는 객체가 메모리에서 해제(deallocation) 된다면 사이드 테이블이 객체를 nil로 할당받기에 null safety 하다는 장점이 있지만 처음 약한 참조로 접근시에 사이드 테이블 생성 및 할당, 객체를 사이드 테이블을 통해 접근해야 한다는 점, 객체를 nil로 할당하는 zeroing weak 과정을 진행해야 한다는 점 등으로 인해 속도라는 성능 측면에서 손해를 볼 수 있습니다.

미소유 참조는 객체 자체를 바로 참조하기에 dangling pointer가 되었을 때는 치명적인 오류를 발생시킬 수 있지만 직접 참조 방식이기 때문에 속도가 약한 참조에 비해 빠르고 메모리 사용량을 줄일 수 있다는 장점이 있습니다.

Closure에서 Capture List 작성

클로저 내부에 객체를 캡쳐하는 경우 순환 참조가 발생할 수 있습니다. 클로저와 클래스 모두 참조 타입이기 때문에 발생합니다.

기본적으로 클로저 표현식은 해당 값의 강한 참조를 사용하여 주변 범위에서 상수와 변수를 캡쳐합니다. 하지만 캡쳐 리스트를 사용하면 클로저에서 값이 캡쳐되는 방식을 명시적으로 변경 가능합니다.

캡처 리스트의 항목은 클로저가 생성될 때 값을 복사하여 초기화됩니다. 즉, 캡처 리스트의 각 항목에 대해 상수는 주변 범위에서 이름이 같은 상수 또는 변수로 초기화됩니다.

클로저에서 순환 참조를 해결하기 위해서는 캡쳐 리스트에 weak와 unowned 키워드를 사용하여 클로저 내부에서 객체를 참조하는 방식을 변경합니다.

3. deinit 메서드는 언제 호출되며, 어떤 역할을 하나요?

deinit 메서드는 객체가 메모리에서 해제될 때 오버라이드 없이 자동으로 호출되는 소멸자이며, 파라미터나 반환값을 가질 수 없습니다.
Swift에서는 객체가 해제되기 직전에 deinit 메서드가 자동으로 호출되며, deinit은 다음과 같은 상황에서 유용하게 사용됩니다.

  1. 리소스 해제: 파일 핸들, 네트워크 연결, 데이터베이스 연결과 같은 리소스를 해제하는 데 사용됩니다.
  2. Observer 제거: NotificationCenter나 KVO에서 추가된 옵저버를 제거하여 메모리 누수를 방지합니다.
  3. 디버깅: 객체의 해제 여부를 확인하는 로그를 추가하여 순환 참조가 발생하는지 확인하는 데 유용합니다.

@Hoon94 Hoon94 added the LEVEL 1 label Jan 6, 2025
@Hoon94 Hoon94 self-assigned this Jan 6, 2025

기본적으로 클로저 표현식은 해당 값의 강한 참조를 사용하여 주변 범위에서 상수와 변수를 캡쳐합니다. 하지만 캡쳐 리스트를 사용하면 클로저에서 값이 캡쳐되는 방식을 명시적으로 변경 가능합니다.

캡처 리스트의 항목은 클로저가 생성될 때 값을 복사하여 초기화됩니다. 즉, 캡처 리스트의 각 항목에 대해 상수는 주변 범위에서 이름이 같은 상수 또는 변수로 초기화됩니다.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

값 의미론과 참조 의미론이 동일하게 동작하는가에 관해서도 다루면 좋을 것 같아요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[LEVEL 1] 19. ARC
2 participants