[Swift] 메모리관리 (ARC)

Kiwi·2024년 3월 20일

Swift

목록 보기
19/21
post-thumbnail

한정된 메모리를 잘 관리하기 위해..

📁 ARC (Automatic Reference Counting)

스위프트의 ARC는 메모리 관리 기법 중 하나이다. 영어 그래도 자동 참조 카운팅, 즉 참조를 자동으로 카운팅하여 메모리를 관리한다는 것이다..! 🫤 ARC의 동작 원리를 보면 먼저 클래스의 새로운 인스턴스가 생성될 때마다 ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리를 할당한다. 이 메모리는 해당 인스턴스와 관련된 저장된 프로퍼티의 값과 함께 인스턴스의 타입에 대한 정보를 가진다. 그리고 인스턴스가 더 이상 필요치 않을 때 ARC는 메모리를 해제한다. 그러나 아직 사용중인 인스턴스의 할당을 해제하면 실제로 인스턴스에 접근하려고 할때 앱에 크래시가 발생한다.

어떻게 ARC는 메모리를 할당하고 해제할 시점을 아는걸까??🤔 바로 객체나 인스턴스가 참조되는 횟수를 추적하는 것이다. 객체가 생성될 때마다 참조 횟수가 1 증가하고, 해당 객체를 참조하는 다른 객체나 변수가 없어지거나 더 이상 사용되지 않을 때 참조횟수가 1 감소한다. 참조횟수가 0이 되면 해당 객체는 메모리에서 해제된다. (참조 카운팅은 클래스의 인스턴스에만 적용된다. 구조체와 열거형은 참조타입이 아니고 값타입)

ARC는 인스턴스에 참조가 하나라도 존재하는 한 인스턴스를 할당 해제하지 않는다. 이것을 가능하게 하려면 프로퍼티, 상수, 또는 변수에 클래스 인스턴스를 할당할 때마다 해당 프로퍼티, 상수, 또는 변수는 인스턴스에 강한 참조를 만든다. 참조는 해당 인스턴스를 유지하고 강한 참조가 남아있는 한 할당 해제를 허용하지 않는다.

🗂️ ARC 동작

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) 초기화")
    }
    deinit {
        print("\(name) 초기화해제")
    }
}

var ref1: Person?
var ref2: Person?
var ref3: Person?

Person 클래스를 정의한다. 그리고 Person?타입의 3개의 변수를 정의한다. 이 변수는 옵셔널 타입으로 자동으로 초기화 되고 아직은 인스턴스를 참조하지 않는다.

ref1 = Person(name: "Ally")
//Ally 초기화

ref1 변수는 새로운 Person 인스턴스에 대한 강한 참조가 있다. 하나의 강한 참조가 있기 때문에 ARC는 Person을 메모리에 유지하고 할당 해제하지 않는다.

ref2 = ref1
ref3 = ref1

남은 2개의 변수에 인스턴스를 할당하면 Person 인스턴스에 3개의 강한 참조가 있습니다. 그리고 기존의 참조를 포함하여 2개의 변수에 nil을 할당하여 강한 참조 2개를 중단하면 하나의 강한 참조만 남고 인스턴스는 할당 해제되지 않는다.

ref1 = nil
ref2 = nil

마지막 강한 참조를 중단하면 할당 해제한다.

ref3 = nil
// Prints "Ally 초기화 해제"

🗂️ 강한 참조 사이클

이렇게 강한 참조 카운팅 즉 ARC로만 메모리 관리가 될것같아 보인다... 사실 안된다. 만약 두 클래스 인스턴스가 서로에 대한 강한 참조를 유지하여 각 인스턴스가 다른 인스턴스를 유지하게 하는 사이클이 발생할 수 있다. 이를 강한 참조 사이클이라고 한다. 강한 참조 사이클이 발생하면 메모리가 해제되지 않고 유지되어 메모리 누수가 발생한다.

class Man {
    var name: String
    var gf: Woman?
    
    init(name: String) {
        self.name = name
    }
    deinit { print("man deinit")}
}

class Woman {
    var name: String
    var bf: Man?
    
    init(name: String) {
        self.name = name
    }
    deinit { print("woman deinit!") }
}

var ally: Woman? = .init(name: "ally")
var ben: Man? = .init(name: "ben")

위의 코드를 보자. ally 변수는 새로운 Woman 인스턴스에 대한 강한 참조를 가지고 있고, ben변수는 새로운 Man 인스턴스에 대한 강한 참조를 가지고 있다.

ally?.bf = ben
ben?.gf = ally

이제 각자의 인스턴스에서 여자친구, 남자친구의 관계를 서로가 되도록 연결해준다. 이렇게 되면 다음 그림과 같이 강한참조가 부여된다.

이렇게 되면 두 인스턴스 연결은 서로간의 강한 참조 사이클을 생성한다. 그래서 다음과 같이 두 변수를 nil로 설정하여 변수와 인스턴스간의 참조를 끊어도 참조 카운팅이 0이 되지 않는다.

ally = nil
ben = nil

그래서 다음과 같은 상황이 발생한다.

이런 강한 참조 사이클은 인스턴스가 할당 해제되는 것을 방해하여 앱에서 메모리 누수를 유발한다.

🗂️ 강한 참조 사이클 해결

이런 강한 참조 사이클을 해결하기 위해 swift에서는 2가지 방법을 제공한다. 바로 약한참조와 미소유참조이다.

약한 참조와 미소유 참조를 사용하면 참조 사이클의 한 인스턴스가 강한 유지 없이 다른 인스턴스를 참조할 수 있다. 참조하려는 인스턴스의 수명이 짧은 경우, 즉 먼저 할당 해제된다면 약한 참조를 사용한다. 반대로 참조하려는 인스턴스의 수명이 동일하거나 더 긴 경우 미소유 참조를 사용한다.

약한참조 (weak reference)

참조되는 대상을 약하게 참조하여 순환 참조를 방지하는 방식이다. weak 키워드를 프로퍼티 또는 변수 선언 전에 위치시켜 약한 참조를 나타낸다. 약한 참조는 참조하는 객체를 강제로 유지하지 않고, 참조 대상이 메모리에서 해제되면 자동으로 nil로 설정된다. 이렇게 nil로 변하는 것을 허락해야 하므로 항상 옵셔널 타입의 상수가 아닌 변수로 선언된다.

class Company {
    var name: String
    weak var worker: Worker?
    
    init(name: String) {
        self.name = name
    }
    deinit { print("Company deinit")}
}

class Worker {
    var name: String
    var company: Company?
    
    init(name: String) {
        self.name = name
    }
    deinit { print("Worker deinit!") }
}

var ally: Worker?
var apple: Company?

ally = Worker(name: "ally")
apple = Company(name: "apple")

ally!.company = apple
apple!.worker = ally

위의 코드를 보면 Worker인스턴스는 Company인스턴스에 대해 강한 참조를 가지고 있지만 Company인스턴스는 Worker인스턴스에 약한 참조를 가지고 있다. 그림으로 나타내면 다음과 같다.
이것은 ally 변수에 nil을 설정하여 강한 참조를 끊으면 더 이상 강한 참조가 아니란 의미한다.

미소유참조 (unowned reference)

미소유참조는 참조되는 대상이 항상 유효한 경우에만 사용하며, 해당 대상이 해제될 수 있는 상황에는 사용하지 않는다. 즉, 참조하는 다른 인스턴스의 수명이 같거나 더 긴 경우에 사용된다. 프로퍼티 또는 변수 선언 전에 unowned 키워드를 위치시켜 나타낸다.
미소유참조는 항상 값이 있다고 가정하여, 참조하는 객체가 해제되면 런타임 에러가 발생할 수 있다. (unowned으로 만들어진 값은 옵셔널로 만들어지지 않는다.)

class Company {
    var name: String
    unowned var worker: Worker
    
    init(name: String, worker: Worker) {
        self.name = name
        self.worker = worker
    }
    deinit { print("Company deinit")}
}

class Worker {
    var name: String
    var company: Company?
    
    init(name: String) {
        self.name = name
    }
    deinit { print("Worker deinit!") }
}

var ally: Worker?

ally = Worker(name: "ally")
ally!.company = Company(name: "apple", worker: ally!)

위의 코드에서는 아까와 같이 Worker와 Company라는 클래스를 각각 정의해놓았다. 이때 Company는 Worker가 없으면 돌아가지 않기에 항상 Worker와 연관되어 있다. Company 인슨턴스는 참조하는 Worker 보다 오래 지속되지 않는다. 이것을 표현하기 위해 Company 클래스의 worker 프로퍼티를 unowned로 Worker 프로퍼티로 가진다.

ally변수에 의한 강한 참조를 끊으면 더이상 Worker 인스턴스에 대한 강한 참조가 아니므로 할당 해제된다. 그리고 Company 인스턴스에 대해 더이상 강한 참조가 아니므로 이것도 할당 해제된다.

ally = nil
// Worker deinit!
//Company deinit
profile
🐣 iOS Developer

0개의 댓글