본문 바로가기

iOS

[iOS] 강한 참조 사이클 (순환 참조) 해결하기 1편

인스턴스를 사용하지 않는데도 메모리에서 해제 되지 않으면 메모리 누수(Memory Leak)가 발생합니다. 강한 참조 사이클(Strong Reference Cycle)에 의해 발생할 수 있으며 약한(Weak) 참조와 비소유(Unowned) 참조를 통해 해결할 수 있습니다. 강한 참조에 대한 자세한 설명은 ARC 란? 을 참고해주세요!

 

강한 참조 사이클은 순환 참조(Retain Cycle)라고도 불립니다.

 

예시를 통해 강한 참조 사이클이 발생하는 경우를 먼저 살펴보도록 하겠습니다.

 

class Person {
    var name = "John"
    var apartment: Apartment?

    deinit {
        print("Person deinit")
    }
}

class Apartment {
    var address: String
    var tenent: Person?

    init(address: String) {
        self.address = address
    }

    deinit {
        print("Apartment deinit")
    }
}

 

Person 클래스에는 String 타입의 name 과 Apartment 타입의 apartment 프로퍼티가 존재합니다. Apartment 클래스에는 마찬가지로 String 타입의 address 와 Person 타입의 tenent 가 존재합니다.

 

var person: Person? = Person() // Person 카운트 +1 => 총 +1
var apartment: Apartment? = Apartment(address: "Seoul") // Apartment 카운트 +1 => 총 +1

person?.apartment = apartment // Apartment 카운트 +1 => 총 +2
apartment?.tenent = person // Person 카운트 +1 => 총 +2

person = nil // Person 카운트 -1 => 총 +1
apartment = nil // Apartment 카운트 -1 => 총 +1

 

personapartment 에 nil을 할당하여도 참조 카운트가 모두 +1 이므로 해당 인스턴스는 메모리에서 해제되지 않습니다. 두 인스턴스의 프로퍼티가 서로를 참조하고 있기 때문입니다. 여기서 문제가 발생합니다.

 

personapartment 모두 nil 이므로 더 이상 인스턴스에 접근할 방법이 없어졌습니다. 따라서 두 인스턴스를 정상적으로 메모리에서 해제할 방법이 없어진 것이죠.

 

person = nil
apartment = nil

 

다시 nil 을 할당한다고 해도 참조 카운트가 더 감소하는 것은 아닙니다. 이렇게 강한 참조 때문에 인스턴스를 정상적으로 메모리에서 해제할 수 없는 문제를 강한 참조 사이클이라고 하는 것입니다. ARC 는 메모리 관리를 대신 해주지만 강한 참조 사이클까지 관리해주지는 않습니다.

 

이 문제를 이제 약한 참조와 비소유 참조를 통해 해결해보겠습니다. 두 가지 참조 모두 인스턴스 사이의 강한 참조를 제거하는 방식으로 문제를 해결합니다. 강한 참조와 달리 참조 카운트를 증가시키거나 감소시키지 않는 것이죠. 그러나 참조를 통해 인스턴스에 접근할 수는 있지만 인스턴스가 사라지지 않도록 유지하는 것은 불가능합니다.

Weak Reference

약한 참조는 인스턴스를 참조하지만 소유하지는 않습니다. 참조 카운트에도 영향을 주지 않습니다. 참조하는 인스턴스는 언제든지 메모리에서 해제될 수 있는 것이죠. 그렇기 때문에 옵셔널(Optional) 형식으로 선언합니다.

 

weak var name: Type?

 

참조하고 있는 인스턴스가 메모리에서 해제되면 자동으로 nil로 초기화됩니다. 소유자에 비해 짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용합니다.

 

약한 참조를 이용하여 강한 참조 사이클을 해결해보겠습니다.

 

class Person {
    var name = "John"
    var apartment: Apartment?

    deinit {
        print("Person deinit")
    }
}

class Apartment {
    var address: String
    weak var tenent: Person? // 약한 참조

    init(address: String) {
        self.address = address
    }

    deinit {
        print("Apartment deinit")
    }
}

var person: Person? = Person() // Person 카운트 +1 => 총 +1
var apartment: Apartment? = Apartment(address: "Seoul") // Apartment 카운트 +1 => 총 +1

person?.apartment = apartment // Apartment 카운트 +1 => 총 +2
apartment?.tenent = person // Person 카운트 영향 없음 => 총 +1
// tenent 변수가 Person 인스턴스를 소유하지 않음

person = nil // Person 카운트 -1 => 총 0
// Prints "Person deinit"
// 이 때, person의 apartment 프로퍼티가 제거되면서 Apartment 인스턴스에 대한 소유권을 자동으로 포기함
// 그래서 Apartment 인스턴스의 참조 카운트가 1 감소됨 => 총 +1

apartment = nil // Apartment 카운트 -1 => 총 0
// Prints "Apartment deinit"
// 모두 정상적으로 메모리에서 해제됨

 

Person과 Apartment 인스턴스가 정상적으로 메모리에서 해제되어 강한 참조 사이클에 대한 문제가 해결되었습니다.

Unowned Reference

비소유 참조는 약한 참조와 동일한 방식으로 강한 참조 사이클을 해결합니다. 동일하게 인스턴스를 참조하지만 소유하지 않습니다. 하지만 옵셔널이 아니라 Non-Optional 방식입니다. 참조 사이클을 해결하면서 프로퍼티를 Non-Optional로 선언해야 할 때 사용합니다.

 

unowned var name: Type

 

약한 참조와는 다르게 참조하는 인스턴스가 메모리에서 해제되어도 nil로 초기화되지 않습니다. Non-Optional 이기 때문에 해제된 인스턴스에 접근할 경우 런타임 에러가 발생할 수 있습니다. 인스턴스의 생명주기가 소유자와 같거나 더 긴 경우에 사용합니다.

 

이번에는 비소유 참조를 이용하여 강한 참조 사이클을 해결해보겠습니다.

 

class Person {
    var name = "John"
    var apartment: Apartment?

    deinit {
        print("Person deinit")
    }
}

class Apartment {
    var address: String
    unowned var tenent: Person // 비소유 참조

    init(address: String, tenent: Person) {
        self.address = address
        self.tenent = tenent
    }

    deinit {
        print("Apartment deinit")
    }
}

var person: Person? = Person() // Person 카운트 +1 => 총 +1
var apartment: Apartment? = Apartment(address: "Seoul", tenent: person) // Apartment 카운트 +1 => 총 +1

person?.apartment = apartment // Apartment 카운트 +1 => 총 +2
// apartment의 person 프로퍼티는 비소유 참조이므로 여전히 참조 카운트는 +1

person = nil // Person 카운트 -1 => 총 0
// Prints "Person deinit"
// 이 때, person의 apartment 프로퍼티가 제거되면서 Apartment 인스턴스에 대한 소유권을 자동으로 포기함
// 그래서 Apartment 인스턴스의 참조 카운트가 1 감소됨 => 총 +1

apartment = nil // Apartment 카운트 -1 => 총 0
// Prints "Apartment deinit"
// 모두 정상적으로 메모리에서 해제됨

 

약한 참조와 같은 방식으로 강한 참조 사이클을 해결한 것을 알 수 있습니다.

정리

  • 강한 참조 사이클을 해결하는 방법에는 약한 참조와 비소유 참조가 있습니다.
  • 약한 참조는 Optional 로 선언하고 비소유 참조는 Non-Optional 로 선언합니다.
  • 비소유 참조는 런타임 에러가 발생할 위험이 존재합니다.