[Swift] 메모리관리 - ARC(Automatic Reference Counting)

Yuni·2023년 12월 20일

메모리

: 메모리의 힙 영역에 할당된 데이터는 명시적으로 관리해야만 메모리에서 해제가 됩니다. 이를 위해 참조 횟수(Reference Count, RC)를 세어 메모리 관리를 수행하며, 런타임 시 참조 횟수를 기반으로 메모리 해제 여부를 결정합니다. 만약 메모리에서 해제되지 않으면 메모리 누수 현상이 발생할 수 있습니다.

☑️ 메모리 누수현상이란??
: 힙에 올라간 데이터들이 사라지지 않는 현상 & 사라지게 할 수 있는 방법이 없는 상황

ARC

: ARC는 특정 클래스 객체를 참조하는 변수, 상수, 프로퍼티들의 참조 횟수가 0이 되는 시점에 자동으로 힙에서 메모리를 해제합니다. 클래스 인스턴스가 메모리에서 해제될 때 즉시 deinit 함수가 호출됩니다.

  • ARC의 단점은 순환 참조가 발생할 경우 메모리가 영구적으로 해제되지 않을 수 있다는 점입니다.

1. ARC가 자동으로 객체를 해제하는방법

: 실행 후 RC가 0이 되면 자동으로 객체를 메모리에서 해제합니다.

class Cat {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) 메모리 해제")
    }
}

func doSomething() {
    var cuke = Cat(name: "cuke") // cuke 참조카운트 1
    }
    
    doSomething()

2.직접 nil을 할당하여 수동으로 객체를 해제하는 방법

: 객체에 nil값을 할당하면 RC가 0개가 되기 때문에 자동으로 메모리 해제됩니다.

var cuke: Cat? = Cat(name: "cuke") // cuke 참조카운트 1
var yuni: Person? = Person(name: "Yuni") // yuni 참조카운트 1

cuke = nil // cuke 참조카운트 0 (더 이상 Cat 객체를 참조하지않음.)
yuni = nil // yuni 참조카운트 0 (더 이상 yuni 객체를 참조하지않음.)

강한참조(Strong References)

: 인스턴스의 주소값이 변수에 할당되면 강한 참조가 발생하며, 이는 참조 횟수를 증가시킵니다. 이 때문에 인스턴스끼리 서로를 가리킬 수 있으며, 이러한 상황이 순환 참조를 발생시킬 수 있습니다.
실제 데이터는 힙에 저장되고, 그 데이터를 참조하는 정보는 스택에 저장됩니다.

class Cat {
    var name: String
    var owner: Person? 
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person {
    var name: String
    var pet: Cat? 
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) 메모리 해제")
    }
}
var cuke: Cat? = Cat(name: "cuke") // cuke 참조카운트 1
var yuni: Person? = Person(name: "Yuni") // yuni 참조카운트 1

cuke?.owner = yuni // cuke 참조카운트 2
yuni?.pet = cuke // yuni 참조카운트 2

/// nil을 할당해줘도 해제가 되지 않음.
/// 서로가 서로를 참조하게 됨 (= 강한참조 사이클이 일어남)
cuke = nil // cuke 참조카운트 1
yuni = nil // yuni 참조카운트 1

약한참조(Weak References)

: 약한 참조는 ARC가 해당 인스턴스에 대한 참조를 해제할 수 있도록 허용하는 참조 방식입니다. 약한 참조로 참조되는 동안에도 해당 인스턴스가 메모리에서 해제될 수 있으며, 이 때 ARC는 해당 참조를 nil로 초기화합니다. 따라서 약한 참조는 언제든지 해제될 수 있으며, 옵셔널 변수에만 사용할 수 있습니다.

  • 아래 예시와 같이 약한 참조는 보통 한쪽 방향으로만 설정하여 사용합니다. 한쪽이 약한 참조로 설정되면, 해당 참조가 가리키는 상대방의 참조 카운트가 증가하지 않으므로 순환 참조를 피할 수 있습니다.
class Cat {
    var name: String
    weak var owner: Person? // 약한 참조
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person {
    var name: String
    var pet: Cat? 
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) 메모리 해제")
    }
}

var yuni: Person? = Person(name: "Yuni") // yuni 참조 카운트 1
var cuke: Cat? = Cat(name: "Cuke") // cuke 참조 카운트 1

// 약한 참조로 설정되어 있기 때문에 참조 카운트가 증가하지 않음
cuke?.owner = yuni // cuke 참조 카운트 1

// 인스턴스 해제 시점
cuke = nil // cuke 참조 카운트 0, Cat 객체 해제
yuni = nil // yuni 참조 카운트 0, Person 객체 해제


위 예시는 Cat 클래스의 owner 프로퍼티는 약한 참조로 선언되어 있습니다. 때문에 Person 객체가 메모리에서 해제될 때 Cat 객체가 자동으로 그 참조를 nil로 초기화하게 됩니다. 이러한 방식으로 순환 참조 문제를 방지하고 메모리 누수를 예방할 수 있습니다.

미소유참조(unowned)

: 미소유 참조는 약한 참조와 유사하지만, 가리키는 인스턴스가 해제되더라도 자동으로 nil을 할당하지 않습니다.

class Cat {
    var name: String
    unowned var owner: Person // 미소유 참조
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }
    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person {
    var name: String
    var pet: Cat?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}

var yuni: Person? = Person(name: "Yuni") // yuni 참조카운트 1
var cuke: Cat? = Cat(name: "cuke", owner: yuni!) // cuke 참조카운트 1

/// 참조카운트가 0이되어 메모리에서 해제됨
cuke = nil // cuke 참조카운트 0
yuni = nil // yuni 참조카운트 0

⚠️ 미소유참조 - 에러발생 상황.

: 참조하던 객체가 메모리에서 먼저 해제된 경우에 런타임 에러발생할 수 있습니다.

  • 메모리에서 해제된 상태에서 실제 구현을 하면 주소에 접근해봤더니 값이 없어서 그냥 앱이 꺼져버리는 것.(= 런타임 에러 발생)

☑️ 가리키는 인스턴스가 메모리에서 해제되지 않을 것이라는 확신이 있을 때 사용해야 합니다.

var yuni: Person? = Person(name: "Yuni") // yuni 참조카운트 1
var cuke: Cat? = Cat(name: "cuke", owner: yuni!) // cuke 참조카운트 1

yuni = nil // yuni 참조카운트 0

// yuni를 먼저 메모리에서 해제한 후에 cuke의 owner에 접근을 시도
print(cuke?.owner.name) // 런타임 에러 발생

0개의 댓글