Swift

Swift | weak / unowned에 관하여

ziziDev 2024. 5. 23. 14:03
반응형

안녕하세요!

 

저번에 다루었던 메모리관리의

ARC에 관하여

다루고자 합니다

 

짧게 말하자면

애플은 이전에 MRC(manual reference count)를 채택하여

수동으로 메모리를 관리하여 모든 메모리 해제 코드를 삽입을 했습니다

여기서 발생하는건 실수할 가능성이 높아

메모리 관리에 대한 부담이 굉장히 높았답니다

그래서 현대적인 언어들은 대부분 자동으로 메모리 관리 모델을 사용하고 있습니다

 

그래서 지금 현재  Swift에서 사용하고 있는건

ARC(automatic reference count)

입니다

retain() 할당하고 release() 해제코드로 메모리 해제를 한다고 생각하시면됩니다

컴파일러가 메모리 관리코드를 시작하기전에 미리 자동으로 추가함으로 프로그램의 메모리 관리에 대한

안정성이 증가하게 됩니다

 

WWDC21에서 다루었던 강한 참조 사이클(Strong Reference Cycle)은 서로를 참조하고 있기에

해제가 되지 않는 현상을 볼 수 있습니다

그래서 변수의 참조에 nil을 할당해도 메모리가 해제 되지않는 메모리 누수가 되는 최악의 상황이 발생하게 되죠

 

이걸 해결하기 위해서 참조 해제를 고려하여 코드를 작성해야하고

약한 참조와 비소유 참조를 사용하여

해결할 수 있습니다

 

이제 이 두 가지 키워드를 알아보기전에 

좀 더 기초적으로 다루고 난 후

약한 참조와 비소유 참조를 알아보도록 합시다

 

Reference counting이 될까?

|

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?

 

 

이렇게 초기값이 nil인 Person 타입의 옵셔널 변수입니다

변수 3개는 아직 Person 객체를 참조하고 있지 않아서 참조카운트와는 아직 관련이 없습니다

 

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") //Person +1 / Rc =1
reference2 = reference1                     //Person +1 / Rc =2
reference3 = reference1                     //Person +1 / Rc =3

reference1 = nil                            //Person -1 / Rc =2
reference2 = nil                            //Person -1 / Rc =1
reference3 = nil                            //Person -1 / Rc =0

//Rc가 0이 되면서
//deinit이 호출됩니다
// Prints "John Appleseed is being deinitialized"

 

반드시 객체의 생성이 시작과 동시에 카운트가 됩니다

그리고 참조 변수가 객체를 참조할 때마다 카운트가 증가됩니다

그리고 참조 변수가 해제될 때마다 카운트가 감소되고

참조 카운트가 0이 되면 객체가 메모리에서 해제되고 deinit 메서드가 호출하게 됩니다

 

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

 

 

클래스 인스턴스 간의 강력한 참조 순환을 가리키고 있습니다

왜 강한 순환이라고 말하는걸까? 라고 생각이 들 수 있습니다

코드를 통해서 좀 더 알아보도록 합시다

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") }
}
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed") 		//Person +1 / Rc =1
unit4A = Apartment(unit: "4A")		  	//Apartment +1 / Rc =1

john!.apartment = unit4A			//Apartment +1 / Rc =2
unit4A!.tenant = john				//Person +1 / Rc =2

john = nil					//Person -1 / Rc =1
unit4A = nil					//Apartment -1 / Rc =1

 

 

인스턴스를 생성하고 새 인스턴스를

john / unit4A 변수에 할당할 수 있습니다

 

 

할당한 후 강력한 참조를 하고 있습니다

 

 

 

서로를 참조할 수 있도록 서로의 변수에 참조하도록 설정합니다

두 인스턴스를 서로 연결하게되면 아래 그림처럼 서로를 참조하게 됩니다

 

 

이렇게 연결하게되면 둘 사이에 강력한 참조 순환이 발생합니다

여기서 메모리 해제를 한다고 해도 RC는 절대 0이 될 수 없는걸 확인할 수 있습니다

 

 

 

클래스 인스턴스 간의 강력한 참조 순환 해결을 하기 위해서

약한 참조와 비소유 참조를 제공하고 있습니다

 

약한 참조 먼저 알아볼까요?

 

Weak Reference

|

약한 참조

 

짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용하고

인스턴스를 nil로 확인이 가능하며 만약 nil인 경우 즉시 작업을 중단할 수 있습니다

weak reference는 let(상수)를 사용하지 못하며

반드시

optional

선언해야합니다

class Dog {
    
    // 사실상 변수 앞에 (strong) 생략
    // 서로 가리키는 reference counting 증가시키지 않기 위해 weak 사용
    var name: String // 사실상 var (strong) name: String
    
    // 변수 앞에 weak 키워드 추가
    // 서로 참조하더라도 카운팅이 되지 않음
    weak var owner: Person? // weak keyword : 약한 참조
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) memory deallocated")
    }
}

class Person {
    var name: String
    weak var pet: Dog? // weak keyword : 약한 참조
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) memory deallocated")
    }
}

// Dog 객체 choco 생성, 참조 카운트 1
var choco: Dog? = Dog(name: "초코") // choco의 reference count: 1

// Person 객체 zoozoo 생성, 참조 카운트 1
var zoozoo: Person? = Person(name: "주주") // zoozoo의 reference count: 1

// 강한 참조 사이클이 발생하지 않도록 weak reference 사용
choco?.owner = zoozoo // zoozoo의 reference count 변화 없음 (weak 참조)
zoozoo?.pet = choco // choco의 reference count 변화 없음 (weak 참조)


//안전하게 메모리해제됨 ⭐️한쪽만 weak 선언해도됨

// choco를 nil로 설정, choco의 reference count 0, 메모리 해제
choco = nil // choco의 reference count: 0 -> deinit 호출, "초코 memory deallocated"

// zoozoo의 pet는 nil, choco가 해제되었기 때문
zoozoo?.pet // nil

// zoozoo를 nil로 설정, zoozoo의 reference count 0, 메모리 해제
zoozoo = nil // zoozoo의 reference count: 0 -> deinit 호출, "주주 memory deallocated"

 

이렇게 코드를 작성하게되면 weak을 선언했기 때문에

자동적으로 RC가 올라가지 않으며 안전하게 해제되는걸 볼 수 있습니다

 

또 다른 예제를 가지고 다뤄볼까요?

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

 

 

 

현재 Apartment에서 weak 약한 참조를 볼 수 있습니다

 

 

 

이렇게 나타낼 수 있습니다

 

여기서 john을 값이 없는 변수로 만들면 어떻게 될까요?

 

 

인스턴스에 대한 강력한 참조가 더이상 없기 때문에 Person 할당이 해제되며

deinit()이 호출하게 됩니다

 

 

 

그리고 아파트를 해제하면 어떻게 될까요?

 

 

아파트는 강력한 참조를 중단하면 인스턴스에 대한 강력한 참조가 더 이상 없기 때문에

아파트 또한 해제되며 deinit()이 호출하게 됩니다

 

 

 

다음으로 비소유 참조에 대해서 알아봅시다

 

Unowned Reference

|

비소유 참조

 

비소유 참조는 다른 인스턴스의 생명주기가 더 길거나 같은 경우에 사용을 합니다

비소유 참조로 표시되었다고 값이 옵셔널로 되지않고 ARC는 절대로 참조의 값을 nil로 설정하지 않습니다

 

실제 인스턴스가 해제가 되었을 때 호출하게되면 에러가 발생하게 됩니다

옵셔널로 선언하는것도 가능하지만 자동적으로 nil을 할당하고 있진 않습니다

 

그리고 비소유 참조가 nil값으로 설정 되고

이 참조를 사용하려고 하면 프로그램이 멈출 수 있습니다

 

비소유 참조는 항상 참조가 참조하는 객체의 수명을 고려해야합니다

참조하는 객체가 사라지면 비소유 참조는 더이상 유효하지 않습니다

 

그래서 비소유 참조를 사용할 때 항상 유효한 객체를 가리키는지를 확인해야합니다

 

class Dog1 {
    var name: String
    // Swift 5.3 이전 버전에서는 비소유참조의 경우 옵셔널 타입이 선언 ❌
    unowned var owner: Person1? // unowned 키워드 : 비소유 참조
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) memory deallocated")
    }
}

class Person1 {
    var name: String
    unowned var pet: Dog1? // unowned 키워드 : 비소유 참조
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) memory deallocated")
    }
}

// Dog1 객체 choco1 생성, 참조 카운트 1
var choco1: Dog1? = Dog1(name: "초코1") // choco1의 reference count: 1

// Person1 객체 zoozoo1 생성, 참조 카운트 1
var zoozoo1: Person1? = Person1(name: "주주1") // zoozoo1의 reference count: 1

// 강한 참조 사이클이 발생하지 않도록 unowned reference 사용
choco1?.owner = zoozoo1 // zoozoo1의 reference count 변화 없음 (unowned 참조)
zoozoo1?.pet = choco1 // choco1의 reference count 변화 없음 (unowned 참조)

// choco1을 nil로 설정, choco1의 reference count 0, 메모리 해제
choco1 = nil // choco1의 reference count: 0 -> deinit 호출, "초코1 memory deallocated"

// zoozoo1을 nil로 설정, zoozoo1의 reference count 0, 메모리 해제
zoozoo1 = nil // zoozoo1의 reference count: 0 -> deinit 호출, "주주1 memory deallocated"

 

unowned또한 RC 상승하지 않는걸 볼 수 있습니다

하지만 비소유의 경우 참조하고 있던 인스턴스가 사라지면 nil로 초기화되진 않습니다

 

에러가 발생하는 케이스를 알아볼까요?

 

 

Fatal error: Attempted to read an unowned reference but object 0x600000c20c60 was already deallocated

 

이미 해제가 되었기 때문에 접근하려고하면

에러가 발생하게 됩니다

 

그래서 에러가 발생하지 않기 위해서

각각 nil을 설정해야 에러가 발생하지 않습니다

 

 

또 다른 예시를 하나 더 볼까요?

 

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


var john: Customer?


john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

 

 

 

 

위 코드를 어떻게 연결되어있는지 볼 수 있습니다

서로를 연결하고 있지만

 Creditcard의 속성인 customer이 unowned로 연결되어있는걸 확인할 수 있습니다

 

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

 

 

소유되지 않는 참조로인해 customer가 보유한 strogn 참조를 중단하고

인스턴스 john에 대한 strong 참조가 더 이상 없어집니다

 

 

 

 

비소유 속성인 customer 비소유 참조 덕분에 john 변수가 보유한 강한 참조가 해제되면

Customer 인스턴스에 대한 강한 참조가 더이상 없어지게 됩니다

 

결론적으로 Customer 인스턴스에 대한 강한 참조가 없기 때문에

Customer 인스턴스가 해제됩니다

 

이후에 CreditCard 인스턴스에 대한 강한 참조도 더이상 없기 때문에 해제됩니다

 

john 변수가 nil로 설정된 후에 Customer 인스턴스와 CreditCard 인스턴스의 소멸자가

모두 "소멸됨" 메시지를 출력하는 것을 보여주고 있습니다

 

 

또 다른 예시를 볼까용?

아래 코드는 클래스에 대한 옵셔널로 비소유 참조를 표시할 수 있답니다

weak과 차이점은

비소유 참조 옵셔널을 사용할 때 항상 유효한 객체를 참조하거나 nil로 설정해야합니다

 

weak의 경우에는 다른 객체가 해당 강의를 참조하지 않는 경우에 메모리에서 해제될 수 있도록 할 수 있습니다

 

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]

 

학교에서 제공하는 강좌를 추적하고 있는 예시입니다

Department는 학과가 제공하고 있는 각 강의에 대한 강한 참조를 유지하고 있는걸 볼 수 있습니다

 

 

 

 

 

'unowned optional reference'는 클래스 인스턴스에서 강한 참조를 유지하지 않고

ARC가 해당 인스턴스를 해제하는걸 강제로 막고 있진 않습니다

unowned는 optional reference 가 nil이 될 수 있다라는 점입니다

 

unowned optional 은 nil이 될 수 있기에 항상 deallocated되지 않는 한 인스턴스를 참조하는지 확인하는 것이 중요합니다

 

여기서 department.courses에서 강의를 삭제할 때 다른 강의가 해당 강의에 대한 참조를 제거해야합니다

 

// 강의를 삭제할 때 해당 강의를 참조하고 있는 다른 강의의 참조를 제거하는 함수
func removeCourse(_ course: Course, from department: Department) {
    // department.courses 배열을 순회하면서 각각의 강의의 nextCourse 속성을 확인하고, 삭제할 강의를 참조하고 있는 경우에는 nil로 설정
    for otherCourse in department.courses {
        if let next = otherCourse.nextCourse, next === course {
            // 다른 강의가 삭제될 강의를 참조하고 있는 경우
            otherCourse.nextCourse = nil // 삭제될 강의에 대한 참조 제거
            break
        }
    }
    
    // department.courses 배열에서 삭제될 강의 제거
    if let index = department.courses.firstIndex(where: { $0 === course }) {
        department.courses.remove(at: index)
    }
}

// 예를 들어, intermediate 강의를 삭제할 경우
removeCourse(intermediate, from: department)

 

 

이렇게 제거를 할 수도 있지만

 

 

class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
    
    deinit {
        print("Department \(name) is being deinitialized")
    }
}

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
    }
    
    deinit {
        print("Course \(name) is being deinitialized")
    }
}

// 부서 생성
var department: Department? = Department(name: "Horticulture")

// 강의 생성
var intro: Course? = Course(name: "Survey of Plants", in: department!)
var intermediate: Course? = Course(name: "Growing Common Herbs", in: department!)
var advanced: Course? = Course(name: "Caring for Tropical Plants", in: department!)

// 강의 간의 연결
intro?.nextCourse = intermediate
intermediate?.nextCourse = advanced

// 생성한 강의들을 각 해당 강의 목록에 추가
department?.courses = [intro!, intermediate!, advanced!]

// 객체들 해제
department = nil
intro = nil
intermediate = nil
advanced = nil

 

객체를 생성한 후에 nil로 설정하여 각 객체를 해제하고 있습니다

이 때 객체가 해제되면 해당 객체의 deinit 메서드가 호출됩니당!

 

그리고 또한 옵셔널이 사용가능하면

!*래핑되지않은 옵셔널)도 사용이 가능합니다

 

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"

 

capitalCity 속성은 어떤 나라든 반드시 수도가 있어야 하므로 nil이 될 수 없기에

!의 부호를 붙였습니다

속성을 강제로 설정하여 초기화할 때 해당 클래스의 인스턴스를 바로 할당 할 수 있습니다

 

City의 경우  country 속성은 비소유 참조로 이루어져 있는 이유는

해당 도시가 국가에 속해 있어야 하므로 비소유 참조로 설정이 됩니다

 

국가와 도시 간의 참조 사이클을 방지하는 데 도움이 된다고 생각합니다

그리고 도시가 소유하지 않는 참조를 국가를 가리키므로 도시가 삭제되어도 국가가 삭제되지 않는다는 보장이 있습니다

 

 

그리고 클로저에 대한 강력한 참조순한을 풀기위해

[unowned self] 키워드를 참조하시기 바랍니다

 

// HTMLElement 클래스 정의
class HTMLElement {
    let name: String // HTML 요소의 이름을 저장하는 속성
    let text: String? // HTML 요소 내의 텍스트를 저장하는 선택적 속성
    
    // HTML로 변환된 요소를 나타내는 클로저 속성
    // 클로저는 객체가 처음 사용될 때 한 번만 실행됨
    // self에 대한 unowned 참조를 캡처함
    lazy var asHTML: () -> String = {
        [unowned self] in // unowned 참조를 캡처
        if let text = self.text { // 텍스트가 있는 경우
            return "<\(self.name)>\(text)</\(self.name)>" // HTML 문자열 생성
        } else { // 텍스트가 없는 경우
            return "<\(self.name) />" // 빈 HTML 요소 생성
        }
        
        /*
        //이렇게 사용하는 경우 강한 참조 사이클이 일어나서 메모리 낭비가 생긴다
         if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
        */
    }
    
    // 초기화 메서드
    init(name: String, text: String? = nil) {
        self.name = name // 주어진 이름으로 요소의 이름을 설정
        self.text = text // 주어진 텍스트로 요소의 텍스트를 설정
    }
    
    // 객체가 제거될 때 호출되는 소멸자
    deinit {
        print("\(name) is being deinitialized") // 객체가 제거되었음을 출력
    }
}

// HTMLElement 인스턴스 생성
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML()) // HTML로 변환된 요소 출력
// Prints "<p>hello, world</p>"

paragraph = nil // HTMLElement 인스턴스 제거
// Prints "p is being deinitialized" // 객체가 제거되었음을 출력

 

 

여기서 unowend self를 사용하는 이유는

강한 참조 순환을 방지하기 위해서 입니다

 

 

 

self에 대한 참조를 강하게 캡처하는 문제가 발생될 수 있습니다

클로저가 존재하는 동안에는 객체가 해제되지 않기 때문입니다

 

이러한 문제는 강한 참조 순환을 유발할 수 있고 객체가 메모리에서 해제되지 않아 메모리 누수가 생길 수 있기 때문에

unowned self 를 사용하여 클로저가 self를 캡처할 때 강한 참조가 아닌 약한 참조로 캡처될 수 있도록 하고 있습니다

 

클로저의 실행이 유지되는 동안 객체의 수명을 관리하기에 적합합니다

 

하지만 해제가 된 후에도 클로저가 접근을 시도할 경우 런타임 오류가 발생할 수도 있기 때문에

해당 객체의 수명이 클로저보다 긴 경우에만 안전하게 사용해야합니다

 

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
    }
    
    lazy var printName: () -> Void = { [unowned self] in
        print(self.name) // 클로저 내에서 self에 접근
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

var person: Person? = Person(name: "John")

// 클로저 실행
person?.printName() // 출력: "John"

// 객체 해제
person = nil // Person 객체가 해제됨

// 클로저 실행 - 객체는 이미 해제되었으나 클로저는 여전히 객체에 접근하려고 함
person?.printName() // 런타임 오류 발생: "Fatal error: Attempted to read an unowned reference but object <Person> has already been deallocated"

 

 

이렇게 간략하게 weak / unowned에 대해서 알아봤습니다

 

 

❤️혹시나 잘못된 부분이 있다면 댓글로 알려주면 감사하겠습니다❤️

 

 

 

 

 

 

✏️참고

앨런스위프트 문법 자료(강의)⭐️⭐️⭐️ -추천

애플 공식 문서

반응형