본문 바로가기

iOS/스터디

[Swift] 순환참조에 대해 알아보자 (feat. strong, weak, unowned reference)

300x250

순환 참조란 두 가지 이상의 객체가 서로에 대한 Strong Reference(강한 참조) 상태를 가지고 있을 때 발생하며, 순환 참조가 발생하게 되면 서로에 대한 참조가 해제되지 않기 때문에 메모리에서 유지되며 이로 인해 메모리 릭이 발생하게 된다.

 

이러한 순환 참조를 해결하기 위해 weak, unowned reference가 사용된다. 

☞ 클로저에서의 weak self

 


1. 하나의 인스턴스에 대한 참조

먼저, 다음과 같이 Person 클래스를 만들어 보자.

 

import UIKit

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

 

name 프로퍼티를 initializer로 가지는 Person 클래스를 만들었다.

 

그다음 옵셔널 Person? 타입을 가질 수 있는 옵셔널 변수를 정의하고 출력해본다.

 

var student1: Person?
var student2: Person?
var student3: Person?

print(student1) ///nil 출력
print(student2) ///nil 출력
print(student3) ///nil 출력

 

student1, student2, student3 변수는 다음과 같은 특징을 가지게 됨을 알 수 있다.

  1. student1, student2, student3 변수는 옵셔널 Person? 타입으로 선언되며 nil 값으로 할당되었다.
  2. Person 인스턴스로 참조되지 않는다.

 

이제 student1에 Person 인스턴스를 할당해보자.

 

student1 = Person(name: "철수") 
///철수 is being initialized 출력

 

"철수 is initialized"가 출력되는 것으로 Person 클래스의 initializer가 호출되었음을 알 수 있고, deinit 내부의 print함수가 호출되지 않은 것으로 보아 deinitializer는 호출되지 않았음을 알 수 있다.

 

왜 그런 것일까?

student1 변수와 Person 인스턴스의 관계

새로운 Person 인스턴스가 student1에 할당되며 student1과 Strong reference(강한 참조)를 가지게 되었다.

Person 인스턴스가 student1에 대한 강한 참조를 가지게 되며 ARC는 Person 클래스를 메모리에 유지하고 deallocated 시키지 않게 만들었다.

 

 

이 상태에서 student1이 가지고 있는 참조를 해제하면 어떻게 될까?

 

student1 = nil
///철수 is being deinitialized 출력

 

student1이 가지고 있던 참조를 해제하면서 Person인스턴스는 어떠한 참조도 가지고 있지 않게 되고 deinitializer가 호출된다.

 

 

다음과 같이 하나의 인스턴스에 두 가지 이상의 변수들을 할당해보자.

student2 = student1
student3 = student1

//메모리 주소 출력
print(Unmanaged.passUnretained(student1!).toOpaque()) ///0x0000600002a77c00출력 
print(Unmanaged.passUnretained(student2!).toOpaque()) ///0x0000600002a77c00출력 
print(Unmanaged.passUnretained(student3!).toOpaque()) ///0x0000600002a77c00출력

student1, student2, student3 모두 같은 메모리 주소를 참조하고 있다는 것을 알 수 있다.

 

student2, student3에 Person 인스턴스를 참조하고 있는 student1을 할당함으로써 Person 인스턴스에 세 개의 강한 참조가 걸리게 되며, Person 인스턴스는 자신에게 할당된 세 개의 강한 참조가 해제되기 전까지는 deallocate 되지 않는다.

student1 = nil
student2 = nil

 

위처럼 student1, student2가 참조 해제되어도 Person 인스턴스는 아직 student3에 대한 강한 참조를 유지하고 있으므로 deallocated 되지 않았다.

student3 = nil
///철수 is being deinitialized 출력

마지막으로 Person 인스턴스에 참조된 student3의 참조를 해제하게 되면 deinitalizer가 호출되는 것을 알 수 있다.

 

이것으로 인스턴스에 대한 참조는 여러 개가 생길 수 있고, 해당 인스턴스를 deinit 시키기 위해서는 해당 인스턴스가 가진 모든 참조를 해제시켜야 한다는 것을 알 수 있다.

 

 

 


2. 클래스 인스턴스 간의 강한 순한 참조

위의 예제에서 보듯이, ARC(Automatic Reference Counting)는 Person 인스턴스에 대한 참조의 개수를 트래킹 하고, Person 인스턴스가 더 이상 필요하지 않게 되면(참조가 0이 되면) deallocate 시킨다는 것을 알 수 있다.

 

그렇다면 두 클래스 인스턴스가 서로에 대한 참조를 유지하게 된다면 어떻게 될까?

두 클래스 인스턴스가 서로에 대한 강한 참조를 가지고 있어서 각 인스턴스가 다른 인스턴스를 계속 유지하게 되는 경우를 강한 순환 참조(strong retain cycle)라고 한다.

 

강한 순환 참조가 발생하는 경우에 대해서 알아보기 위해 다음과 같이 Person, Car 클래스를 정의해보자.

 

import UIKit

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

class Car {
    let model: String
    init(model: String) {
        self.model = model
    }
    var owner: Person?
    
    deinit {
        print("Car \(model) is being deinitialized")
    }
}
  • Person 클래스는 String 타입의 name 프로퍼티와 nil로 초기화된 옵셔널 Car? 타입의 car 프로퍼티가 있다.
  • Car 클래스는 String 타입의 model 프로퍼티와 nil로 초기화된 옵셔널 Person? 타입의 owner 프로퍼티가 있다. 

 

그리고 각각 Person?, Car? 타입을 가지는 변수를 정의하고 Person, Car 인스턴스를 할당해보자.

 

var 철수: Person?
var 테슬라S: Car?

철수 = Person(name: "철수")
테슬라S = Car(model: "테슬라S")

철수 변수와 Person인스턴스, 테슬라S 변수와 Car 인스턴스의 관계

이후, person에 car를, car에 owner를 할당시켜보자.

철수!.car = 테슬라S
테슬라S!.owner = 철수

* (!)를 사용하여 옵셔널 변수인 철수, 테슬라S를 언랩핑하여 각 인스턴스에 접근하고 각 인스턴스의 car, owner 프로퍼티에 값을 저장한다.

 

 

이렇게 되었을 때 각 인스턴스 간 참조는 다음과 같이 이루어진다.

강함 참조 사이클 관계를 가지게 된 두 인스턴스

Person 인스턴스는 Car 인스턴스에 대해 강한 참조를 가지게 되고, Car 인스턴스는 Person 인스턴스에 대해 강한 참조를 가지게 되며 두 인스턴스 사이에 강한 참조 사이클이 생겼다.

 

이때 각 변수에 할당된 레퍼런스 카운트는 다음과 같다.

<철수>

1. Person 인스턴스를 강한 참조 하며 reference count 증가 (reference count: 1)

2. Person 인스턴스가 Car 인스턴스에 대해 강한 참조를 가지게 되며 reference count 증가 (reference count: 2)

 

<테슬라 S>

1. Car 인스턴스를 강한 참조 하며 reference count 증가 (reference count: 1)

2. Car 인스턴스가 Person 인스턴스에 대해 강한 참조를 가지게 되며 refrence count 증가 (reference count: 2)

 

두 변수에 nil을 할당하여 소유권을 포기하게 되면 어떻게 될까?

철수 = nil
테슬라S = nil
///각 변수의 deinit이 호출되지 않음
  • 철수를 nil로 할당하여 소유권을 포기하고 메모리에서 해제하려고 했지만, Car 인스턴스가 철수에 대한 강한 참조를 가지고  있어(reference count: 1) 메모리에서 사라지지 않는다.
  • 테슬라S 또한 nil로 할당하여 소유권을 포기하고 메모리에서 해제하려고 했지만, Person 인스턴스가 테슬라S에 대한 강한 참조를 가지고 있어(reference count: 1) 메모리에서 사라지지 않는다.

위와 같이 서로에 대한 강한 참조로 인해 인스턴스를 제대로 해지할 수 없는 상태가 강한 순환 참조(strong retain cycle) 상태이다.

ARC는 이러한 강한 순환 참조에 대한 메모리 관리를 해주지 않기 때문에 약한 참조(weak reference), 비소유 참조(unowned reference)를 사용해서 해결해야 한다.

 

2-1. 약한 참조(Weak References)

  • 약한 참조는 참조하는 인스턴스에 대해 참조를 강하게 유지하지 않아서 ARC가 해당 인스턴스에 대한 참조를 해제할 수 있도록 하여 강한 순환 참조 상태를 막을 수 있다.
  • 변수 혹은 속성 앞에 weak 키워드를 둠으로써 weak reference 임을 나타낼 수 있다.
  • 참조하는 인스턴스에 대한 참조를 강하게 유지하지 않기 때문에 약한 참조로 참조되고 있는 동안에도 해당 인스턴스가 할당 해제될 수 있다.
  • ARC는 인스턴스가 할당 해제될 때 해당 인스턴스를 약한 참조하는 프로퍼티를 nil로 초기화한다.
  • 그리고 이것 때문에 약한 참조는 항상 옵셔널 변수에만 가능하며, 상수에는 사용할 수 없다.
  • 약한 참조는 언제든지 해제될 수 있기 때문에 reference count를 증가시키지 않는다.

위의 Car 클래스의 owner 프로퍼티를 약한 참조로 바꿔보자.

class Car {
    let model: String
    init(model: String) {
        self.model = model
    }
    weak var owner: Person? //약한 참조로 선언
    
    deinit {
        print("Car \(model) is being deinitialized")
    }
}

 

철수, 테슬라S 두 변수의 관계는 다음과 같았다.

var 철수: Person?
var 테슬라S: Car?

철수 = Person(name: "철수")
테슬라S = Car(model: "테슬라S")

철수!.car = 테슬라S
테슬라S!.owner = 철수

 

owner 프로퍼티를 약한 참조로 변경하였을 때 두 인스턴스 간의 관계는 다음과 같이 변한다.

 

Person 인스턴스는 여전히 Car 인스턴스에 대해 강한 참조를 유지하고 있지만, Car 인스턴스는 Person 인스턴스에 대해 약한 참조를 가지게 된다.

이 말은 철수 변수를 nil로 만들어서 Person 인스턴스에 대한 강한 참조를 깨게 되면, Person 인스턴스는 더 이상 강한 참조를 가지지 않게 된다.

철수 = nil
//철수 is being deinitialized 출력

더 이상 Person 인스턴스에 대한 강한 참조가 없으므로, Person 인스턴스가 deallocate 되었고 Person인스턴스에 대해 약한 참조를 가지던 owner 프로퍼티는 nil이 된다.

Car 인스턴스에 대한 강한 참조는 테슬라S 변수가 가지는 참조만 남게 된다.

이 강한 참조를 깨게 되면, Car 인스턴스는 더 이상 강한 참조를 가지지 않게 되고 deallocate 된다.

테슬라S = nil
//Car 테슬라S is being deinitialized 출력

 

2-2. 무소유 참조(Unowned References)

  • 약한 참조와 같이 무소유 참조도 인스턴스에 대한 강한 참조를 유지하지 않는다.
  • 무소유 참조는 다른 인스턴스와 생명주기가 같거나 더 긴 경우에 사용한다.
  • 변수 혹은 속성 앞에 unowned 키워드를 둠으로서 무소유 참조(unowned reference) 임을 나타낸다.
  • 무소유 참조는 항상 값을 가지고 있다고 가정한다.
  • 무소유 참조 변수는 옵셔널 타입이 아니며, ARC도 모소유 참조로 선언된 값을 nil로 만들지 않는다.
  • 할당 해제되지 않은 인스턴스를 참조한다고 확신하는 경우에만 무소유 참조를 사용한다.
  • 무소유 참조로 참조하는 인스턴스가 할당 해제된 후 해당 값에 접근하려고 하면 런타임 오류가 발생한다.

무소유 참조를 알아보기 위해 Customer, CreditCard 클래스를 정의해보자.

  • Customer는 CreditCard가 있을 수도, 없을 수도 있다.
  • CreditCard 클래스는 항상 customer가 존재해야 하며 순환 참조를 피하기 위해 unowned를 사용하여 customer 상수를 정의했다. 
import Foundation

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

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

이후 영희라는 변수를 만들고, Customer 클래스를 할당해보자.

var 영희: Customer?
영희 = Customer(name: "영희")
영희!.card = CreditCard(number: 1234_5678_9012_1234, customer: 영희!)

이렇게 되었을 때 두 인스턴스의 관계는 다음과 같다.

영희는 Customer 인스턴스에 대해 강한 참조를 가지고 있고, CreditCard 인스턴스에 customer에 대한 무소유 참조 Cusomer 인스턴스가 있다.

 

영희가 Customer 인스턴스에 대한 강한 참조를 해제하면 어떻게 될까?

영희 = nil
//영희 is being deinitialized 출력
//Card #1234567890123456 is being deinitialized 출력

Customer, CreditCard 인스턴스에 대한 강한 참조가 모두 해제되었다.

Customer에 대한 강한 참조가 해제되며 Customer 인스턴스에 대해 강한 참조가 더 이상 없으므로 할당 해제되었다.

이후 CreditCard 인스턴스에 대한 강한 참조도 없어지므로 CreditCard 인스턴스도 할당 해제되었다.

 

 

 

☞ 예제 소스코드 github


참고자료
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

 

320x100