Tong's Blog

[iOS] ARC(Automatic Reference Counting)이란? 본문

iOS/Swift

[iOS] ARC(Automatic Reference Counting)이란?

통스 2020. 4. 27. 23:37
반응형

오늘은 Swift의 메모리 관리를 위해 사용하는 ARC에 대해 알아보겠습니다.

 

우선 Swift의 공식문서를 살펴보죠

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

 

Automatic Reference Counting — The Swift Programming Language (Swift 5.2)

Automatic Reference Counting Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself. A

docs.swift.org

 첫 문단에서 보면 앱의 메모리 사용량을 추적하고 관리하는 방식이라고 나옵니다. 특히 자동으로 관리해준다는 점이 매우 흥미롭네요. 그렇다면 Swift는 ARC로 어떻게 메모리 관리를 해줄까요?

 

 우선, Reference Counting이라는 말을 이해해야 합니다. 메모리를 할당하거나 메모리 포인터를 참조할 때 이 레퍼런스의 카운트를 증가시키고 참조가 사라지거나 사용이 끝나면 카운트를 감소시킨다는 이야기입니다. 그리고 만약 카운트가 0이 된다면 그 때 메모리에서 해당 인스턴스가 완전히 사라지고 다시 사용할 수 있는 상태가 된다는 것입니다.

 

 단순히 개념만 가지고는 어떤 방식인지 이해하기가 어렵네요. 공식문서에 있는 코드와 함께 보면서 이해해보죠

우선 Person이라는 class를 생성합니다.

(아 이 ARC의 개념은 Reference라는 단어에서도 알 수 있듯이 참조타입의 인스턴스에서만 해당하는 이야기입니다. struct나 enum같은 값타입에서는 해당 개념이 적용되지 않습니다!)

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

var reference1: Person?
var reference2: Person?
var reference3: Person?

Person클래스 아래에 Person 타입의 변수를 정의했습니다. 아직 Person 인스턴스를 생성하지 않았기에 레퍼런스 카운팅이 올라가지 않습니다. 그럼 이제 해당 변수에 인스턴스를 할당해보겠습니다.

reference1 = Person(name: "John Appleseed")
//레퍼런스 카운트 1 증가 0 -> 1

reference2 = reference1
//레퍼런스 카운트 1 증가 1 -> 2
reference3 = reference1
//레퍼런스 카운트 1 증가 2 -> 3

주석에서 설명하듯이 새로 인스턴스를 생성할때 레퍼런스 카운트가 1 증가하고 해당 인스턴스에 참조될때도 레퍼런스 카운트가 1씩 증가합니다. 위 코드에서는 총 3의 레퍼런스 카운트가 생겼다고 할 수 있겠네요. 자 그럼 해당 참조가 차지 하는 메모리를 다시 사용하려면 어떻게 해야 할까요?

 

위에서 설명한대로라면 레퍼런스 카운트를 0으로 만들어야 합니다.

reference1 = nil
//레퍼런스 카운트 1 감소 3 -> 2
reference2 = nil
//레퍼런스 카운트 1 감소 2 -> 1

 자, 위 코드를 보면 처음엔 할당 reference1를 nil로 바꿨으니 해당 참조가 사라질수도 있다고 생각하시는 분 계신가요?(해당 개념을 이해하기에 접니다...) ARC에서는 단순히 3이였던 카운트를 2로 감소된 것일뿐이죠. 그럼 위 코드는 2개의 레퍼런스 카운트를 감소시켰으므로 아직 레퍼런스 카운트가 1이되고 결국 아직 메모리는 할당 해제가 되지 않았습니다.

reference3 = nil
//레퍼런스 카운트 1 감소 1 -> 0

마지막 reference3까지 해제해야 비로소 해당 인스턴스가 할당 해제가 되는 것입니다.

 

 그렇다면 우리는 ARC만 믿고 메모리 관리는 Swift에 맡겨두면 될까요?

그러면 너무나도 좋겠지만 세상은 늘 그랬듯이 쉬운 일만 있지 않네요 ㅠㅠ, 몇가지 경우에 Reference Counting이 0이 되지 않는 상황이 생기고 해당 메모리 영역을 해제하지 못하게 되는 경우가 있습니다.

 

자 다시 공식 문서를 보면서 우리가 해야할 일이 무엇인지 봐야겠죠.

우선 우리는 참조의 방식에 대해 알아야할 필요가 있습니다.

 

  • Strong
  • Weak
  • Unowned

자 우리가 일반적으로 생성하는 변수 및 상수 레퍼런스는 strong 참조방식을 취하고 있습니다. 이 strong 참조는 우리가 위에서 계속 봐왔던 Reference Counting을 증가시킵니다. 반대로 weak와 unowned는 Reference Counting을 증가시키지 않습니다. 그렇다면 strong을 사용했을 때 어떤 문제가 생길까요?

 

바로 "순환 참조"에 의한 메모리 누수입니다.

다시 공식문서에 나오는 코드를 살펴보죠 Person과 Apartment 두가지 클래스가 있네요.

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") }
}

Person은 Aparment의 인스턴스를 가지고 있고(아직 할당되지는 않았지만) Apartment는 Person의 인스턴스를 가지고 있습니다. 두 변수 모두 weak나 unowned 키워드가 없기 때문에 strong 방식으로 참조 선언됬습니다.

var john: Person?
var unit4A: Apartment?

//인스턴스 할당 레퍼런스 카운트 1 증가
john = Person(name: "John Appleseed")	//1
unit4A = Apartment(unit: "4A")		//1

//참조 레퍼런스 카운트 1 증가
john!.apartment = unit4A		//2
unit4A!.tenant = john			//2

자 이렇게 인스턴스 생성과 참조를 통해 john과 unit4A는 각각 2의 레퍼런스 카운트를 가지고 있습니다. 근데 만약 john과 unit4A에게 nil를 할당하면 어떻게 될까요?

//레퍼런스 카운트 1씩 감소
john = nil	//1
unit4A = nil	//1

자 john과 unit4A는 nil를 부여했지만 아직 레퍼런스 카운트가 1이기 때문에 해당 인스턴스는 메모리에서 해제되지 않았습니다. 그리고 더 이상 해제될 수도 없습니다. 이런 경우를 "순환 참조"라고 하고 메모리 누수로 이어지는 것입니다.

 

이러한 순환 참조를 방지하기 위해서 weak과 unowned를 사용해야 합니다.

weak과 unowned는 참조시 Reference Count를 증가시키지 않기 때문에 위와 같은 코드에서 메모리를 해제할 수 있게됩니다.

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 }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

이렇게 한 방향에만 weak를 부여해도 순환 참조를 막을 수 있게됩니다.

 

그렇다면 weak와 unowned의 공통점은 알았는데 차이점은 무엇일까요?

weak는 옵셔널이 방식이고 unowned는 비옵셔널인 차이점이 있습니다. 따라서 용도에 맞게 사용하시면 될 것 같습니다.

 

오늘은 공식문서를 통해 ARC은 기능과 참조의 방식 및 메모리 누수 방지등 여러가지 내용을 한꺼번에 다루어 보았습니다. 지금 당장 모든 것을 한번에 이해하는 것은 어려우니 공식문서를 보면서 해당 코드를 짜보면 조금이나마 도움이 될 것 같습니다.

 

감사합니다.

 

P.S : 잘못된 부분에 대해서는 언제나 피드백 및 비판을 환영합니다.

반응형
Comments