[iOS] Swift에서의 메모리 관리 방법(ARC)에 대해 설명하고, 순환 참조를 방지하기 위한 전략

Zerom·2024년 1월 17일
0

iOS 질문 답변

목록 보기
3/9
post-thumbnail

내가 생각한 대답

=> Swift에서는 Auto Reference Counting, 즉 ARC를 통해 메모리를 관리하는데 ARC란 Swift에서 컴파일 타임에 자동으로 retain, release를 삽입해서 런타임에 reference count를 증감시키다가 count가 0이 되면 deinit을 통해 메모리를 해제시키는 방식으로 메모리를 관리해주는 것입니다. 인스턴스의 주소 값이 변수에 할당될 때 RC값이 증가하면 이것을 '강한 참조'라고 하는데 두 객체가 서로 강한 참조를 하며 서로의 RC값을 증가시키면 순환참조가 일어나게 되고 이 때는 weak나 unowned를 통해 참조를 하더라도 RC 값을 증가시키지 않도록 해야지 메모리에서 정상적으로 해제되서 메모리 누수를 방지할 수 있습니다.

추가 질문

순환 참조를 해결하기 위한 weak와 unowned 참조의 차이점은 무엇인가요?

  • weak (약한 참조)
    - 인스턴스를 참조할 때 RC를 증가시키지 않음
    - 참조하던 인스턴스가 메모리에서 해제된 경우, 자동으로 nil이 할당되어 메모리가 해제
    - 프로퍼티를 선언한 이후, 나중에 nil이 할당된다는 관점으로 봤을 때 weak는 무조건 옵셔널 타입의 변수여야함
    - 순환 참조이지만 weak로 선언되어 RC 값을 올리지 않는 것을 '약한 순환 참조'라고 함
    - 두 인스턴스 중 수명이 더 짧은 인스턴스를 가리키는 애를 약한 참조로 선언함
  • unowned (미소유 참조)
    - 강한 순환 참조를 해결할 수 있고, RC값을 증가시키지 않는 것은 weak와 동일
    - weak와는 다르게 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신할 때 사용
    - 만약 참조하는 도중에 참조하던 인스턴스가 메모리에서 해제된다면 nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있기 때문에 위험성이 높음
    - 강한 순환 참조가 발생한 경우 weak와는 반대로 둘 중 수명이 더 긴 인스턴스를 가리키는 애를 미소유 참조로 선언함

=> weak와 unowned는 모두 RC값을 증가시키지 않아 강한 순환 참조를 해결할 수 있지만 참조하던 인스턴스가 메모리에서 해제된 경우 weak는 자동으로 nil이 할당되어 메모리가 해제되지만 unowned는 nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있기 때문에 에러가 발생할 수 있어 위험성이 높습니다.

ARC를 효과적으로 관리하기 위한 코드 작성 방법은 어떤 것이 있나요?

=> ARC를 효과적으로 관리하기 위해서는 강한 참조 순환을 해결해야 합니다. 이를 해결 하기 위해서는 weak와 unowned를 사용하고, 클로저 안에서는 두 참조를 캡처 리스트(capture list)를 통해 사용합니다. 그리고 불필요한 객체를 메모리에서 해제 해주는것도 효과적인 관리 방법 중 하나인데 이를 위해 deinit 메서드를 활용하거나 앱이 메모리 경고를 받았을 때 호출되는 didReceiveMemoryWaring 메서드를 활용합니다.

참고

ARC란? - Auto Reference Counting

  • 자동으로 메모리 관리를 해주는데 객체에 대한 참조 카운트를 관리하고 0이 되면 자동으로 메모리에서 해제하는 방식입니다. 런타임에서 계속 실행되는 것이 아닌 컴파일 타입에서 실행
  • ARC는 메모리 영역 중 힙 영역을 관리
  • 힙 영역의 메모리를 관리하는 방법은 GC와 RC가 있는데 ARC는 RC에 포함됨
    - GC와 RC의 가장 큰 차이점은 참조를 계산하는 시점
    - GC는 런타임에 참조 계산을 하기 때문에 인스턴스가 해제될 확률이 RC에 비해 높다는 장점이 있지만 개발자가 참조 해제 시점을 파악할 수 없고 런타임 시점에 계속 추적하는 추가 리소스가 필요하기 때문에 성능 저하가 발생할 수 있음
    - RC는 컴파일타임에 참조 계산을 하기 때문에 개발자가 참조 해제 시점을 파악할 수 있고, 런타임 시점에 추가 리소스가 발생하지 않는다는 장점이 있지만 순환 참조가 발생하면 영구적으로 메모리가 해제되지 않을 수 있음
  • ARC는 Refernce Count를 이용하는데 해당 인스턴스를 현재 누가 가리키고 있는지 없는지를 숫자로 나타낸 것으로 0이 되면 메모리에서 해제하라는 뜻이고, 그렇기 때문에 모든 인스턴스는 자신의 RC 값을 가지고 있음 - 인스턴스가 생성될 때 힙에 같이 저장됨
  • Count Up (+)
    - 인스턴스를 새로 생성할 때
    - 기존 인스턴스를 다른 변수에 대입할 때
  • Count Down (-)
    - 인스턴스를 가리키던 변수가 메모리에서 해제되었을 때
    - (옵셔널 타입) nil이 지정되었을 때
    - 변수에 다른 값을 대입하였을 때
    - 프로퍼티의 경우, 속해 있는 클래스 인스턴스가 메모리에서 해제될 때

메모리 구조

  • 프로그램이 실행되면 운영체제는 메모리에 해당 프로그램을 위한 공간을 할당해 줌

  • 해당 공간은 코드, 데이터, 힙, 스택 총 4가지 영역으로 나눠져 있음

  • 코드 - 우리가 작성한 소스 코드가 기계어 형태로 저장, 컴파일 타임에 결정되고, 중간에 코드가 변경되지 않도록 Read-Only 형태로 저장됨

  • 데이터 - 전역변수, static 변수가 저장, 일반적으로는 프로그램 시작과 동시에 할당되고, 프로그램이 종료 되어야 메모리에서 해제, 실행 도중 변수 값이 변경될 수 있기 때문에 Read-Write로 저장
    - 다만 Swift에서 static을 포함한 전역변수는 기본 동작이 lazy이기 때문에 프로그램 시작과 동시에 할당되어 메모리에 올라가진 않고 해당 값에 처음 접근할 때 값이 할당되어 메모리에 올라감

  • 힙 - 프로그래머가 할당/해제 하는 메모리 영역, malloc, calloc으로 힙에 메모리를 할당할 수 있으며, 이를 '동적 할당' 이라고 함, 사용하고 난 후에는 반드시 메모리 해제를 해줘야 하고 그렇지 않으면 메모리 누수가 발생, 유일하게 런타임 시 결정되기 때문에 데이터의 크기가 확실하지 않을 때 사용
    - Swift에서 클래스 인스턴스, 클로저 같은 참조 타입의 값들은 힙에 자동 할당됨
    - 해제는 ARC를 통해 힙에 할당된 메모리가 더 이상 참조되지 않으면 자동으로 해제해줌
    - 장점으로는 메모리 크기에 대한 제한이 없음
    - 단점으로는 할당 및 해제 작업으로 인한 속도 저하, 힙 손상(이중 해제, 해제 후 사용 등) 작업으로 인한 속도 저하, 힙 경합(두 개 이상 쓰레드가 동시에 접근하려 할 때 Lock이 걸림)으로 인한 속도 저하, 메모리를 직접 관리해야 하는 것들이 있음 -> 여기서 속도 저하란 막 엄청 느리다는 것이 아니고 스택보다 상대적으로 느리다는 개념
    - 자신의 영역 외로 확장하려다보면 힙 오버 플로우 발생

  • 스택 - 함수 호출 시 함수의 지역변수, 매개변수, 리턴 값 등등이 저장되고, 함수가 종료되면 저장된 메모리도 해제됨, 컴파일 타임에 결정되기 때문에 무한히 할당할 수 없음, LIFO(Last In, First Out) 데이터 구조
    - 장점은 CPU에 의해 관리되고 최적화 되서 속도가 매우 빠르고 메모리를 직접 해제를 해주지 않아도 됨
    - 단점은 메모리 크기에 대한 제한이 있고, 지역 변수만 액세스 가능
    - 스택에 너무 많은 메모리를 할당하게 되면 스택 오버 플로우가 발생함 - iOS에서 스택 오버 플로우가 발생하면 어플이 죽어버림

  • 힙과 스택은 같은 메모리 영역을 공유 - 힙 영역은 낮은 메모리 주소부터 할당, 스택 영역은 높은 메모리 주소부터 할당

강한 참조

  • 인스턴스의 주소값이 변수에 할당될 때, RC가 증가하면 '강한 참조'

순환 참조

  • 두 개의 객체가 서로가 서로를 참조하고 있는 형태
  • 이렇게 되면 두 객체가 생성되면서 RC가 +1이 되고, 서로 참조하면서 추가로 +1이 되서 RC가 2가 되는데 객체를 없애려고 RC를 -1 하더라도 RC가 0이 안되기 때문에 ARC가 메모리에서 해제하지 않게 됨 => 힙에서 사라지지 않고 계속 메모리를 소모함
  • 심지어 RC를 -1 하는 과정에서 두 인스턴스는 사라져서 이제 접근할 방법조차 없어지기 때문에계속해서 메모리 누수가 발생

심화

  • ARC의 매커니즘은 Swift Runtime이라는 Library에 구현되어 있는데 Swfit Runtime은 동적 할당되는 모든 object를 HeapObject라는 Struct로 표현함
  • HeapObject에서는 Swift에서 객체를 구성하는 모든 데이터, 즉 reference count와 type meta data를 포함하고 있음
    - 실제로 HeapObject.h에 들어가보면 RefCount.h를 import하고 있고 RefCount에 들어가보면 getWeakRefCount()나 getUnownedRefCount() 같은 함수가 정의되어 있음
  • 따라서 class에 대한 HeapObject를 통해 reference count를 관리 가능

캡처 리스트

  • closure가 정의되는 시점에 복사되는 변수들의 list를 말하는데 capture list에서 나열된 변수의 복사본들은 closure가 메모리에서 소멸되는 순간까지 내용이 변경되지 않은 채로 유지
profile
꼼꼼한 iOS 개발자 /
Apple Developer Academy @ POSTECH 2기 / 멋쟁이사자처럼 앱스쿨 1기

0개의 댓글