메모리 관리를 위한 ARC와 참조 이해하기

고라니·2024년 1월 24일
0

TIL

목록 보기
58/67

ARC

ARC는 앱에서 사용하는 메모리를 추적하고 관리하는데 사용되는 Automatic Reference Counting의 약자이다.
더 이상 사용되지 않는 인스턴스를 메모리에 방치하지 않게 하고, 메모리 성능을 최적화 하는데 도움이 된다.


ARC가 뭘까? 어떻게 작동할까?

ARC는 참조 카운팅(reference counting)을 통해 사용되지 않는 클래스 인스턴스를 자동으로 해제한다. 이는 참조 관계를 카운팅하는 방식이기 때문에 참조 타입인 클래스에서만 작동한다.

ARC작동 원리

  • 인스턴스가 생성될 때 인스턴스의 정보를 담는 데 필요한 메모리를 힙에 할당한다. 이때 인스턴스에 대한 정보와 관련된 프로퍼티 값도 가진다(참조 카운트 값을 포함)
  • 인스턴스가 사용되지 않을 때 그 인스턴스가 차지하고 있는 메모리를 해제하여 공간을 확보해야 한다. 이때 아직 사용중인 인스턴스를 메모리에서 해제하면 문제가 발생할 수 있다.(크래시 발생)
  • 위의 상황을 방지하기 위해 얼마나 많은 프로퍼티가 인스턴스에 대한 참조를 가지는지 추적한다. ARC가 이 작업을 수행하며 참조 카운트가 하나라도 있다면 메모리에서 해제하지 않는다.

예제로 보자

// 다음 코드 예시는 ARC의 작동 방식에서 참조 카운트 변화와 그에 따른 메모리 해제 상태를 보여준다.
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        print("\(name)인스턴스가 메모리에 할당 됨")
        self.name = name
        self.age = age
    }
    
    deinit {
        print("\(name)인스턴스가 메모리에서 해제 됨")
    }
}

var person1 = Person(name: "고라니", age: 2)
// "고라니 인스턴스가 메모리에 할당 됨" 출력
// Person 인스턴스의 참조 카운트 1 증가, 참조 카운트 1


var person2 = person1
// Person 인스턴스의 참조 카운트 1 증가, 참조 카운트 2

person1 = nil
// person 인스턴스의 참조 카운트 1 감소, 참조 카운트 1

person2 = nil
// person 인스턴스의 참조 카운트 1 감소, 참조 카운트 0
// "고라니 인스턴스가 메모리에서 해제 됨" 출력

// 메서드
func printName() {
	let person = Person(name: "고저니", age" 10)
    // "고저니 인스턴스가 메모리에 할당 됨"
    // Person 인스턴스의 참조 카운트 1 증가, 참조 카운트 1
    print(person.name)
}
// 함수 종료, Person 인스턴스의 참조 카운트 1 감소, 참조 카운트 0
// "고저니 인스턴스가 메모리에서 해제 됨" 출력

예시를 보면 참조를 하는 변수의 개수만큼 참조 카운트가 증가하고 반대로 참조를 하는 변수의 개수가 감소하면 참조 카운트도 감소하고 0이 되면 메모리에서 해제되는것을 볼 수 있다.
함수의 경우 함수 내부에서 참조 카운트를 증가시켰지만 함수가 종료되고 해당 변수가 해제되면서 참조 카운트도 감소하여 메모리에서 해제되는 것을 알 수 있다.


참조의 종류: strong, weak, unowned

변수에 클래스를 할당하면 참조 카운트가 증가한다고 했는데 이는 강하게 참조했기 때문이다. 참조 카운트를 증가시키지 않고 클래스를 참조하는 방법도 있다.

강한 참조(strong reference)

기본 참조 타입으로 strong으로 참조하면 강한 참조가 된다.
strong이라고 명시하지 않아도 기본적으로 strong이다.
참조 카운트를 증가시켜 인스턴스를 참조중이라면 해당 인스턴스는 메모리에서 해제되지 않는다.

약한 참조(weak reference)

weak로 참조하게 되면 강하게 참조하지 않아 참조 카운트를 증가시키지 않는다.
var 앞에 weak를 명시해주면 된다.
참조중이던 인스턴스가 메모리에서 해제된 경우 nil로 설정된다.
참조중에 해당 인스턴스가 메모리에서 해제될 가능성이 있다.
런타임 중 nil이 될 수 있기 때문에 항상 'var'로 선언한다.
옵셔널 바인딩을 통해 안전하게 사용 가능하다.

미소유 참조(unowned reference)

weak와 유사하게 강하게 참조하지 않아 참조 카운트를 증가시키지 않는다.
var나 let 에 "unowned"를 명시해주면 된다.
weak와 다르게 참조중이던 인스턴스가 메모리에서 해제된 경우 nil로 설정되지 않는다.
참조하던 인스턴스가 항상 해제되지 않는것이 보장된 상태에서 만 사용해야 한다.
-> 만약 메모리에서 해제된 인스턴스에 접근하면 '런타임 에러'가 발생한다.

예시

// 다음 코드 예시는 ARC의 작동 방식과 strong, weak, unowned 참조의 차이점을 보여준다.

class Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        print("\(name)인스턴스가 메모리에 할당 됨")
        self.name = name
        self.age = age
    }

    deinit {
        print("\(name)인스턴스가 메모리에서 해제 됨")
    }
}

weak var weakPerson: Person?
unowned var unownedPerson: Person!
var strongPerson: Person?

weakPerson = Person(name: "고라니", age: 2)
// "고라니 인스턴스가 메모리에 할당 됨" 출력
// "고라니 인스턴스가 메모리에서 해제 됨" 출력
// 참조 카운트가 증가하지 않아 0이 되기 때문에 바로 메모리에서 해제된다.

unownedPerson = Person(name: "고라니", age: 2)
// "고라니 인스턴스가 메모리에 할당 됨" 출력
// "고라니 인스턴스가 메모리에서 해제 됨" 출력
// 참조 카운트가 증가하지 않아 0이 되기 때문에 바로 메모리에서 해제된다.

strongPerson = Person(name: "고라니", age: 2)
// "고라니 인스턴스가 메모리에 할당 됨" 출력
// 참조 카운트가 1 증가, 인스턴스 생성

weakPerson = strongPerson
unownedPerson = strongPerson

strongPerson = nil
// "고라니 인스턴스가 메모리에서 해제 됨" 출력
// 참조 카운트 1 감소, 참조 카운트 0, 메모리에서 해제

//print(weakPerson.name)
// 옵셔널 타입이기 때문에 안전하게 옵셔널 바인딩 해야 함
if let name = weakPerson?.name {
    print(name)
} else {
    print("이름을 찾을 수 없습니다.")
}

print(unownedPerson.name)
// 런타임 에러 발생!

참조 종류를 나눈 이유가 무엇일 까?
강한 순환 참조와 관련이 있다.


강한 순환 참조(strong reference cycle)

인스턴스의 참조 카운트가 0이 되어야만 메모리에서 해제한다고 했다.
그렇다면 두 객체가 서로를 참조하게 된다면 어떻게 될까?
앱이 종료되기 전까지 참조 카운트가 1 이상이 되고 메모리에서 해제되지 않게된다.
결국 'memory leak'이 발생하게 된다.

// 다음 코드 예시는 강한 순환 참조가 발생하는 예시 상황을 보여준다.
class Person {
    let name: String
    var idCard: IdCard?
    
    init(name: String, age: Int) {
        print("\(name)인스턴스가 메모리에 할당 됨")
        self.name = name
        self.age = age
    }
    
    deinit {
        print("\(name)인스턴스가 메모리에서 해제 됨")
    }
}


class IdCard {
	let idNumber: Int
	var person: Person?
    
    init(idNumber: Int) {
    	self.idNumber = idNumber
        
        print("신분증 인스턴스가 메모리에 할당 됨")
    }
    
    deinit {
    	print("신분증 인스턴스가 메모리에서 해제 됨.")
}

var journey: Person?
var journeyIdCard: IdCard?

journey = Person(name: "journey")
// "journey 인스턴스가 메모리에 할당 됨" 출력

journeyIdCard = IdCard(idNumber: 1)
// "신분증 인스턴스가 메모리에 할당 됨" 출력

// journey 참조 카운트: 1, journeyIdCard 참조 카운트: 1

journey.idCard = journeyIdCard
journeyIdCard.person = journey
// 서로가 서로를 참조, 참조 카운트 1씩 증가

// journey 참조 카운트: 2, journeyIdCard 참조 카운트: 2

// Person 인스턴스와 journeyIdCard 인스턴스를 nil로 할당
john = nil
journeyIdCard = nil

// journey 참조 카운트: 1, journeyIdCard 참조 카운트: 1
// 인스턴스들이 메모리에서 해제되지 않음

애플은 강한 순환 참조를 방지하는 두 가지 방법을 제공한다.
바로 weak와 unowned다. 이 둘은 참조 카운트를 증가시키지 않기 때문에 순환 참조를 방지할 수 있다.


클로저에서의 강한 순환 참조

이전에 클로저의 캡처에 대해서 정리한적이 있었. 클로저가 클래스 인스턴스의 프로퍼티나 메소드를 캡처할 때 self를 참조하면, 이 self는 강한 참조를 가지게 된다. 이런 경우 인스턴스와 클로저의 캡처간 강한 순환 참조가 발생하게 된다.

// 다음 코드 예시는 클로저가 self를 캡쳐할 때 강한 순환 참조가 발생하는 상황을 보여준다.

class Person {
    let name: String
    var completion: (() -> Void)?
    
    init(name: String) {
        self.name = name
        print("\(name) 인스턴스가 메모리에 할당됨")
    }
    
    deinit {
        print("\(name) 인스턴스가 메모리에서 해제됨")
    }
    
    func doSomething() {
        completion = {
            // 클로저가 self를 캡쳐
            print("\(self.name)가 무언가 한다.")
        }
    }
}

var person: Person? = Person(name: "journey")
// person 참조 카운트 증가, 참조 카운트 1

person?.doSomething()
person?.completion?()
// completion이 self를 캡쳐
// person 참조 카운트 증가, 참조 카운트 2

person = nil
// person 참조 카운트 감소, 참조 카운트 1

// 인스턴스가 메모리에서 해제되지 않음

그러면 클로저의 순환 참조를 방지하기 위한 방법은 뭘까?

// // 다음 코드 예시는 캡처리스트를 사용하여 클로저의 순환 참조를 방지하는 방법을 보여준다.

func doSomething() {
    completion = { [weak self] in
        // 클로저가 self를 캡쳐
        print("\(self.name)가 무언가 한다.")
    }
}

캡처리스트를 사용하여 캡처하는 값을 weak로 참조 할 수 있고 순환 참조를 방지할 수 있다.


마치면서

메모리 관리는 앱의 성능에 중요한 영향을 미친다. 그렇기 때문에 ARC를 이해하고 적절하게 활용할 수 있어야 한다. 직접 메모리를 해제할 필요가 없게 도와주는 대신 순환 참조의 문제가 발생할 수 있기 때문에 순환 참조가 발생할 수 있는 상황을 대비하고 적절하게 처리하는 습관을 기르는 것이 필요하다.

profile
🍎 무럭무럭

0개의 댓글