🔥 강한 참조 순환 이해하기

540자
7분

앞선 예제들에서 ARC는 새로 생성된 Person 인스턴스를 참조하는 강한 참조의 수를 추적하여, 더 이상 필요하지 않을 때 해당 인스턴스를 할당 해제할 수 있었어요.

하지만 때로는 클래스 인스턴스가 서로를 강하게 참조하고 있어서, 강한 참조 수가 절대 0이 되지 않는 경우가 있답니다. 이를 강한 참조 순환(strong reference cycle)이라고 해요.

강한 참조 순환은 클래스 인스턴스 간의 관계를 약한 참조(weak reference)나 소유되지 않은 참조(unowned reference)로 정의하면 해결할 수 있지요. 이에 대해서는 클래스 인스턴스 간 강한 참조 순환 해결하기에서 자세히 다룰 거예요. 하지만 그 전에, 강한 참조 순환이 어떻게 발생하는지 이해하는 것이 중요하답니다.

아파트 단지와 거주자를 모델링한 PersonApartment 클래스를 정의한 다음 예제 코드를 통해, 의도치 않게 강한 참조 순환이 발생하는 상황을 살펴볼게요:

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

코드를 하나씩 분석해 볼까요?

  • 모든 Person 인스턴스는 String 타입의 name 속성과 초기값이 nil인 옵셔널 apartment 속성을 가져요.
    • apartment 속성이 옵셔널인 이유는 사람이 항상 아파트를 갖고 있지는 않기 때문이에요.
  • 마찬가지로 모든 Apartment 인스턴스는 String 타입의 unit 속성과 초기값이 nil인 옵셔널 tenant 속성을 가지고 있죠.
    • tenant 속성이 옵셔널인 이유는 아파트가 항상 거주자를 갖고 있지는 않기 때문이랍니다.
  • 두 클래스는 deinitializer를 정의하고 있는데, 이는 해당 클래스의 인스턴스가 할당 해제될 때 이를 출력하도록 해요.
    • 이를 통해 PersonApartment 인스턴스가 기대한 대로 할당 해제되는지 확인할 수 있겠죠?

이제 아래 코드 조각에서는 johnunit4A라는 옵셔널 타입의 변수를 정의하고 있는데, 이는 아래에서 특정 ApartmentPerson 인스턴스로 설정될 거예요. 두 변수는 옵셔널이기에 초기값이 nil이에요:

var john: Person?
var unit4A: Apartment?
swift

이제 특정 Person 인스턴스와 Apartment 인스턴스를 생성하고, 이를 johnunit4A 변수에 할당할 수 있어요:

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

이 두 인스턴스를 생성하고 할당한 후의 강한 참조 상황은 다음과 같아요:

  • john 변수는 새 Person 인스턴스에 대한 강한 참조를 갖고 있죠.
  • unit4A 변수는 새 Apartment 인스턴스에 대한 강한 참조를 갖고 있답니다.

lecture image

이제 두 인스턴스를 연결하여 사람은 아파트를 갖고, 아파트는 거주자를 갖도록 할 수 있어요. 여기서 느낌표(!)는 johnunit4A 옵셔널 변수에 저장된 인스턴스를 강제 추출(unwrap)하여 해당 인스턴스의 속성을 설정할 수 있도록 해줍니다:

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

두 인스턴스를 서로 연결한 후의 강한 참조 상황은 다음과 같네요:

lecture image

안타깝게도, 이렇게 두 인스턴스를 연결하면 이들 간에 강한 참조 순환이 발생하게 됩니다:

  • Person 인스턴스는 이제 Apartment 인스턴스에 대한 강한 참조를 갖게 되죠.
  • Apartment 인스턴스는 Person 인스턴스에 대한 강한 참조를 갖게 된답니다.

따라서 johnunit4A 변수가 갖고 있던 강한 참조를 끊더라도, 참조 수가 0이 되지 않아 ARC에 의해 인스턴스들이 할당 해제되지 않아요:

john = nil
unit4A = nil
swift

이 두 변수를 nil로 설정했을 때 어떤 deinitializer도 호출되지 않은 점에 주목해 보세요. 강한 참조 순환으로 인해 PersonApartment 인스턴스는 할당 해제되지 않고, 앱에서 메모리 누수(memory leak)를 야기하게 됩니다.

johnunit4A 변수를 nil로 설정한 후의 강한 참조 상황은 다음과 같아요:

lecture image

Person 인스턴스와 Apartment 인스턴스 간의 강한 참조는 여전히 남아있고, 이는 끊을 수 없게 되는 거죠.

이렇게 강한 참조 순환은 메모리 관리에서 주의해야 할 중요한 개념 중 하나랍니다. 다행히도 Swift는 약한 참조와 소유되지 않은 참조라는 개념을 통해 이를 해결할 수 있는 방법을 제공하고 있어요. 다음 장에서는 이에 대해 자세히 알아보도록 할게요!