ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS] ARC란 무엇인가 & 메모리 누수
    Apple🍎/iOS 2024. 8. 23. 19:08

    Swift - ARC 한글문서를 참고해서 정리한 문서입니다.

    2024.08.23 - [iOS] 메모리 구조

    앞의 글에서 "하지만 ARC도 강한 참조면 메모리 해제 못한다구?!" 에 이어서 ARC를 알아보자!

    ARC(Automatic Referecne Counting)란?

    객체의 생명주기와 관계를 모델링한다.
    이게 뭔 소리야..? 싶겠지만 앞에서 본 heap영역에 올라간 객체(클래스)의 생명주기, 관계를 모델링 해주는 역할이라고 생각하면 어떨까?
    대부분 Swift의 메모리 관리는 생각할 필요 없이, ARC가 알아서 클래스 인스턴스로 사용된 메모리를 할당/해제 해준다.

    (유의: 참조 카운팅은 클래스의 인스턴스에만 적용! 구조체,열거형은 참조 타입이 아닌 값 타입이므로 참조로 저장되거나 전달되지 않는다.)

    ARC의 작동 원리

    ARC는 메모리 관리를 위해, 코드 간의 관계에 대해 추가적인 정보를 요구한다.
    왜그러냐면, 인스턴스 할당을 해제하면 더이상 인스턴스의 프로퍼티/메서드에 접근할 수 없기 때문에 없는 인스턴스에 접근하면 앱은 크래시가 발생한다.

    따라서, 하나라도 존재하는 한 인스턴스를 할당 해제 하지 않는다.
    그러므로 얼마나 많은 프로퍼티, 상수, 그리고 변수들이 해당 클래스 인스턴스를 참조하고 있는지 알아야한다.

    이걸 알게 해주는 것이 바로 "강한 참조(strong reference)"이다.
    강한 참조란?

    • 프로퍼티, 상수, 변수 = 클래스 인스턴스 형태로 할당할 때마다 해당 프로퍼티, 상수, 변수에 강한 참조를 만든다
    • 강한 참조가 남아있는 한 할당 해제를 하지 않기 때문에 "강한"참조 라고 한다.

    ARC 동작

    자동 참조 카운팅이 어떻게 동작하느냐의 예제이다.
    Person 클래스에 deinit을 두고, 할당 해제할 때 print 문을 찍게 한 예제이다.

    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?
    
    reference1 = Person(name: "John Appleseed")
    // Prints "John Appleseed is being initialized"
    reference2 = reference1
    reference3 = reference1

    reference1 로 참조를 시작하면, Person 인스턴스가 메모리에 할당되고 → 참조 카운트가 1이 된다.
    reference3까지 Person을 참조하는 게 많아지면 +1씩 돼서, 총 3이 된다.

    reference1 = nil
    reference2 = nil
    reference3 = nil
    // Prints "John Appleseed is being deinitialized"

    이후 reference1, 2, 3 을 모두 nil로 하면 Person의 숫자가 3 → 0 이 됨과 동시에 메모리 해제된다.

    하지만 클래스 인스턴스끼리 잘못하면 강한 참조 사이클을 만드는데…

    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 사람에겐 아파트가 유무가 필수는 아니므로, apartment: Apartment? 로 처리해주고
    Apartment에 경우도 공실일 수도 있기 때문에 거주자인 tenant: Person? 으로 처리해준다.

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

    그리고 위와같이 john, unit4A 변수에 각각 인스턴스를 할당해주면, 아래와같은 그림으로 strong reference 관계를 갖는다.

    그러다가 아파트, 거주자 생기게 되면 apartment, tenant를 채우게 되면,

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


    이렇게 서로를 다시 참조하게 되면서 사이클을 만들고 각각의 참조카운트는 Person: 2, Apartment: 2가 된다.

    여기서 john, unit4A에게 nil을 주면
    우리가 생각했을 땐, john, unit4A 객체를 더이상 사용하지 않기 때문에 nil을 주어서 해제했다고 생각하지만

    john = nil
    unit4A = nil


    위와 같이 서로 간의 강한 참조는 남아있고, 메모리 해제가 되지 않는다.
    메모리 해제가 안되기 때문에 deinit의 print 문도 확인할 수 없다..

    강한 참조 사이클의 해결책은?

    그럼 객체 프로퍼티 타입이 클래스인 경우 어떻게 해야하는가..? 강한 참조 사이클을 어떻게 하면 막을 수 있지?
    이 해결책에는 크게 2가지가 있다.

    • weak references (약한 참조)
    • unowned references (미소유 참조)

    약한 참조, 미소유 참조를 이용하면 강한 참조 없이, 다른 인스턴스를 참조할 수 있다.(참조 카운트가 늘어나지 않게 참조할 수 있다)

    보통 weak의 경우 인스턴스의 수명이 더 짧은 (자주 nil이 될 가능성이 많은 경우) 클래스가 약한 참조를 한다.

    약한 참조(Weak References)란?

    • 인스턴스를 약하게 참조해서, 강함 참조 사이클의 일부가 되는 것을 방지한다.
    • 변수 선언 전에 weak 키워드를 붙여서 약한 참조를 나타낸다.
    • 참조하는 동안, 만약 인스턴스가 할당 해제되면 자동으로 변수에 nil을 설정한다.
      그러므로 변수를 weak **var**로 설정해야한다.

    약한 참조 예시

    아까와 같은 코드지만, 이번엔 사람 & 아파트 클래스에서 아파트의 주인이 없어질 가능성이 더 많다고 생각하고 tenant를 weak로 설정한다.

    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") }
    }
    
    var john: Person?
    var unit4A: Apartment?
    
    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")
    
    john!.apartment = unit4A
    unit4A!.tenant = john

    이 상태에서는

    john = Person
    john!.apart = Apart

    unit4A = Apart
    weak unit4A!.tenant = Person
    이므로, Person: 1, Apartment: 2 로 참조카운트가 형성된다.

    그러므로 Person을 혼자서만 참조하던 john을 nil 로 변경하면 Person의 메모리가 해제된다.
    그럼 자연스럽게 unit4A.tenant는 자신이 가리키던 Person이 해제되므로 nil로 변경된다.
    참조카운트 - Person: 0, Apartment: 1

    john = nil
    // Prints "John Appleseed is being deinitialized"

    여기서 unit4A = nil로 변경하면, Apart도 참조가 0으로 되며 메모리가 해제된다.

    unit4A = nil
    // Prints "Apartment 4A is being deinitialized"

    이렇게 약한 참조 weak를 통해 이제야 예상한대로,
    객체를 nil로 했을 때 모두 메모리 해제되는 결과가 나온다.

    그런데 이런 경우가 가능한 이유는
    Person, Apartment 둘다 각 프로퍼티가 optional로 선언 가능한 경우이다.
    즉, 사람들 중엔 아파트가 없는 사람도 있고, 아파트엔 집주인이 아직 없는 아파트도 있기 때문에 가능한 일이다.

    하지만 프로퍼티 중에 반드시 필요한 프로퍼티가 있는 경우 강한 참조 사이클을 안만드려면..?
    ⇒ 미소유 참조를 사용하면 된다.

    미소유 참조(Unowned References)

    • 서로의 클래스 인스턴스의 수명이 같거나, 더 긴 경우의 클래스에 사용한다.
    • 약한 참조와 달리, 항상 값을 갖도록 예상하는 곳에 사용한다.
      • 💡 참조하던 객체의 할당이 해제되면, 자동으로 nil로 변환되던 weak와 달리
        미소유 참조를 하는 변수는 nil로 변경되지 않기 때문에
        항상 할당 해제되지 않는 인스턴스를 참조한다고 확신하는 경우에만 미소유 참조를 사용해야한다!
        만약, 해제된 후에 미소유 참조의 값에 접근하려고 하면 런타임 에러가 발생한다.
    • 변수 앞에 unowned를 붙여 사용한다.

    미소유 참조 예시

    그럼 unowned는 언제 사용하냐
    CreditCard를 만들때 사용자(customer)가 없으면 만들지 못하는 것처럼,
    프로퍼티 속성상 nil이면 안되고 & 반드시 강한 참조가 필요할 때 사용한다!

    class CreditCard {
        let number: UInt64
        unowned let customer: Customer
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
        deinit { print("Card #\(number) is being deinitialized") }
    }
    
    class Customer {
        let name: String
        var card: CreditCard?
        init(name: String) {
            self.name = name
        }
        deinit { print("\(name) is being deinitialized") }
    }
    
    var john: Customer?
    
    john = Customer(name: "John Appleseed")
    john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)


    참조 카운트
    Customer: 1
    CreditCard: 1
    각각 하나씩 참조가 생긴 상태이고, unowned로 인해 사이클이 생기지 않았다.

    여기서 john = nil 로 Customer를 해제시키면,
    Creditcard를 가리키던 Customer 인스턴스가 사라지므로, Customer: 0 으로 해제되고
    Creditcard도 0이되면서 사라진다.
    그러므로 거의 Customer → Creditcard 거의 동시에 할당되므로 문제가 안된다.

    그런데 만약, CreditCard의 클래스 참조카운트가 2라면,
    john = nil → Customer 해제 → CreditCard 참조 카운트 = 1 → unowned let customer 은 Customer 인스턴스를 가리키면서 런타임 에러가 난다.

    이런 식으로 런타임 에러 발생은 치명적이기 때문에 Unowned 방식은 unsafe 하다고 불린다.
    따라서 안전성 검사가 필수적으로 필요하다!

    미소유 옵셔널 참조(Unowned Optional References)

    • unowned var nextCourse: Course? unowned는 옵셔널 변수에도 사용 가능하다.
    • 하지만 참조 대상 객체가 해제되면 안된다는 unowned의 특징은 유지된다.
    • optional 변수에 unowned를 붙이는 이유는 단지 나중에 설정하기 위함이다.

    미소유 옵셔널 참조 예시

    코드를 설명하자면,
    Department - 전공
    Course - 수업

    • Department: 전공에 따라 들어야하는 수업이 있고,
    • Course: 수업은 해당하는 전공(department)이 반드시 있고, 이 수업 이후에 들어야할 수업(nextCourse)이 있을 수도 있고, 없을 수도 있다.
    class Department {
        var name: String
        var courses: [Course]
        init(name: String) {
            self.name = name
            self.courses = []
        }
    }
    
    class Course {
        var name: String
        unowned var department: Department
        unowned var nextCourse: Course?
        init(name: String, in department: Department) {
            self.name = name
            self.department = department
            self.nextCourse = nil
        }
    }
    
    let department = Department(name: "Horticulture")
    
    let intro = Course(name: "Survey of Plants", in: department)
    let intermediate = Course(name: "Growing Common Herbs", in: department)
    let advanced = Course(name: "Caring for Tropical Plants", in: department)
    
    intro.nextCourse = intermediate
    intermediate.nextCourse = advanced
    department.courses = [intro, intermediate, advanced]

    아래 부분처럼 나중에 nextCourse를 설정하기 위함이지, weak 처럼 참조하는 대상 객체가 할당 해제됐을 때 자동으로 nil로 변환시켜주거나 하지 않는다.

    intro.nextCourse = intermediate
    intermediate.nextCourse = advanced

    미소유 참조 & 암묵적 언래핑된 옵셔널 프로퍼티 (Unowned References and Implicitly Unwrapped Optional Properties)

    • PersonApartment 예제는 둘 다 nil 이 될 수 있는 프로퍼티가 강한 참조 사이클을 유발할 수 있는 가능성이 있는 상황을 보여준다.
      ⇒ 이 시나리오는 약한 참조로 해결하는 것이 가장 좋다.
    • CustomerCreditCard 예제는 nil 이 허용되는 하나의 프로퍼티와 nil 일 수 없는 프로퍼티가 강한 참조 사이클을 유발할 수 있는 가능성이 있는 상황을 보여준다.
      ⇒ 이 시나리오는 미소유 참조로 해결하는 것이 가장 좋다.

    하지만 둘다 참조할 때 nil/할당 해제 될 수 없는 관계이면 어떻게 해야할까?
    이럴 땐, unowned & unwrapping property를 이용한다.

    • unowned는 참조 카운트를 안생기게 하지만, 항상 참조할 대상이 있어야한다는 것을 잊지말자!

    아래 코드는 둘다 초기화 할 때 nil이 될 수 없는 참조 관계이다.

    • Country: 항상 수도(capitalCity)가 있고,
    • City: 나라(country)에 항상 속해있어야하기 때문에 둘은 참조 관계를 갖는다.
    class Country {
        let name: String
        var capitalCity: City!
        init(name: String, capitalName: String) {
            self.name = name
            self.capitalCity = City(name: capitalName, country: self)
        }
    }
    
    class City {
        let name: String
        unowned let country: Country
        init(name: String, country: Country) {
            self.name = name
            self.country = country
        }
    }
    
    var country = Country(name: "Canada", capitalName: "Ottawa")
    print("\(country.name)'s capital city is called \(country.capitalCity.name)")
    // Prints "Canada's capital city is called Ottawa"

    그림으로는 아까 CreditCard & Customer의 unowned와 같지만, 과정이 조금 다르다.

    코드를 해석하자면,

    Country의 capitalCity를 암시적인 언래핑(!)을 통해, 초기화 과정에서 잠깐 nil이 될 수 있지만, 초기화 완료된 후에는 반드시 값이 있다는 것을 표현해준다.

    암시적 언래핑(implicitly unwrapped optionals)
    원래는 self 키워드를 사용하려면, 항상 초기화 후에 사용해야하는데, 우리는 초기화에서 self를 사용할 것이기 때문에 암묵적 언래핑(!)을 해주어야한다.
    (만약 언래핑을 안해주면, “'self' used before all stored properties are initialized” 라는 에러를 띄운다.)
     
    암시적 언래핑된 옵셔널 프로퍼티인 capitalCity는 다른 옵셔널 같이 nil을 기본 값으로 가지지만, 초기화 후엔 언래핑 할 필요 없이, 값에 접근할 수 있다는 의미를 가진다.

    따라서, Country에서도 self 를 이용해 City 생성이 가능하며
    City도 unowned를 이용해 Country를 참조하므로 이를 해결할 수 있다.

    정리해보면,,,

    서로를 강하게 참조하는 관계에서는 강한 참조 사이클이 생길 수 있다. 

    이런 사이클을 막기 위해서는 크게 weak, unowned 를 이용해 해결한다.
    그 안에 optional, unwrapping 개념이 경우에 따라 추가될 수도 있다.

    • weak: 보통 nil이 되어도 되는 변수에 붙인다. 필수적인 프로퍼티가 아닌 곳에 weak var person: Person? 같이 이용한다.
    • unowned: 서로를 참조하는 두 객체 중 한쪽이 먼저 사라진다는 보장이 있는 경우, 사라지는 쪽에 unowned를 붙여 사이클을 막는다. 


    이 둘을 사용하려면 서로의 참조 관계를 정확히 파악해야한다.
    서로를 참조할 때 nil이 되어도 되는지, 아닌지에 따라 경우의 수가 나뉘기 때문에 잘 파악해야한다

     

     

    참고 메모

    자동 참조 카운팅 | Swift

Designed by Tistory.