씬디의 블로그

[Swift] ARC 파헤치기 본문

App/Swift

[Swift] ARC 파헤치기

cyndi 2024. 5. 28. 21:23

Swift

면접 단골 질문 ARC (강의자료 44회차)

 

ARC란?

ARC는 Automatic Reference Counting의 약자로

메모리 관리를 자동화하는 기술이다

 

자동화? 어떻게?

Swift에서는 메모리를 추적하고 관리하기 위해 ARC를 사용하고 있고

이름 그대로 Automatic! 자동으로 메모리를 알아서 관리해주기 때문에 개발자는 메모리 관리에 크게 신경 쓸 필요가 없다

 

ARC는 힙 영역에 있는 메모리를 직접 할당하고 해제할 수 있다

힙? 코데힙스부터 알아보자

https://cyndi0330.tistory.com/49

 

[운영체제] 메모리 구조 - 코데힙스

프로그램이 실행되면 운영체제(OS)는 4가지 영역으로 공간을 할당한다 프로그램의 정보를 메모리에 로드해야 하고프로그램이 실행되는 동안 CPU가 코드를 처리하기 위해서는메모리가 명령어와

cyndi0330.tistory.com

 

ARC는 컴파일 타임에 작동한다

내가 정확하게 컴파일 타임과 런타임을 구분하고있을까?

 

Swift는 정적 타입(static type)과 동적 타입(dynamic type)이 있다

정적 타입은 -> 컴파일 타임에

// 정적 타입 - 컴파일 타임
let text: String = 1 // 컴파일 에러

 

동적 타입은 -> 런타임에 발생한다

빌드할 때 발생하는 에러, 내가 자주 마주하던 것 ^ㅡ^

 

ARC는 실행되지 않고 어떻게 RC를 세고 메모리 관리를 할 수 있지?

ARC는 컴파일 타임자동으로 retain, release를 적절한 위치에 삽입하는 방식으로 메모리를 관리한다

 

retain, release,,?

retain

  • 객체의 reference count(retain count)를 증가시킨다
  • 객체가 메모리에서 해제되지 않도록 이 함수를 호출하여 카운트를 증가시킨다

release

  • 객체의 reference count(release count)를 감소시킨다
  • 객체를 더이상 필요로 하지 않을 때 이 함수를 호출하여 카운트를 감소시킨다

컴파일 타임에 코드를 분석하고 예측하여 적절한 위치에 retain, retain을 삽입해주고

런타임에 삽입된 코드가 실행되면서 메모리를 관리한다

 

ARC가 등장하기 전까지 Object-C 시절에는 MRC(Manual Reference Counting)로

retain, release를 통해 수동으로 메모리를 관리했다

Manual -> Automatic으로 변신했구나,,

 

개발자가 retain과 release를 수동으로 작성할 필요 없이

ARC가 객체의 참조 횟수를 추적해서 필요하지 않은 인스턴스가 있다면 메모리에서 자동으로 해제하는 역할을 한다

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil

 

ARC는 인스턴스를 참조하는 횟수만큼 RC를 증가시키고, 인스턴스가 해제될 때 RC를 감소시킨다

 

메모리 관리를 직접 수동으로 하지 않아서 좋고, ARC가 메모리를 알아서 관리해주지만

자동으로 안먹히는 부분(강한 참조 상황)이 있기 때문에 메모리 누수가 일어난다

 

인스턴스가 서로를 참조하는 상황, 이러한 경우를 강한 순환 참조라고 하는데

이게 어떠한 상황이냐면

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil

 

'john''unit4A'가 서로가 서로를 꼬리 물고 있어서 강한 참조 사이클이 발생한다

그래서 nil을 해도 메모리 해제가 되지 않아 메모리 누수가 발생한다

 

왜? 해제되지 않지? 강한 순환 참조가 뭐지?

서로에 대한 강한 참조로 인해 인스턴스를 제대로 해지할 수 없는 상태를 -> 강한 순환 참조라고 한다

 

강한 순환 참조가 문제가 되는 이유는 ARC를 이용해서 RC가 0이 되면 메모리에서 할당 해제를 하는데,

강한 순환 참조가 생기게 되면 RC가 0이 되지 않아

실제로 사용하지 않는 인스턴스가 메모리를 차지하고 있는 메모리 누수 현상이 발생하기 때문이다

 

객체에 대한 강한 참조가 없어지면 ARC는 해당 객체를 메모리에서 해제한다

반면 약한 참조는 객체의 생명 주기에 영향을 주지 않는다

왜냐하면 강한 참조는 객체를 메모리에 유지시키는 반면에, 약한 참조는 참조하고 있는 객체가 해제될 때 자동으로 nil이 되기 때문이다

 

메모리 관리를 최적화 하기 위해서는

weak var로 사용하거나

클로저 내에 weak self로 캡처하는 등의 방법을 적용할 수 있다

 

이러한 방법이 순환 참조를 방지하고, 메모리 누수의 위험을 줄여 앱의 성능과 안전성을 향상시키기 때문이다

class Person {
	// code
	weak var apartment: Apartment?
}

class Apartment {
	// code
    weak var tenant: Person?
}
Class User { 
	var nickname = "Cyndi"
    
    lazy var introduce: () -> String = { [weak self] in
    	return self?.nickname ?? ""
    }
    
    deinit {
    	print("User Deinit")
    }
}

var nickname: User? = User()
nickname?.introduce
nickname = nil

// User Deinit

 

강한 순환 참조를 방지하기 위해서는 약한 참조(weak reference)와 비소유 참조(unowned reference)를 사용할 수 있다

  • 약한 참조(weak): 객체의 생명 주기에 영향을 주지 않으며, 참조하고 있는 객체가 해제될 때 자동으로 nil이 된다. 따라서 항상 변수로 선언해야하며, 옵셔널 타입이어야 한다
  • 비소유 참조(unowned): 객체가 해제될 때 nil로 변환되지 않는다. 객체의 생명 주기에 영향을 주지 않지만, 참조하고 있는 객체가 해제된 후 접근하려고 하면 런타임 에러가 발생할 수 있다

클로저 내에서 강한 참조를 방지하려면 [weak self] 혹은 [unowned self]를 사용하여 메모리 누수를 방지할 수 있다

self, 즉 클로저가 속한 클래스를 약하게 참조하겠다 라는 의미이다