weak와 unowned의 차이는?

weak와 unowned의 차이는?

메모리 참조에 weak와 unowned 참조가 있습니다. weak와 unowned는 모두 class타입에서만 사용할 수 있으며 메모리의 소유를 주장하지 않습니다.

weak 참조

먼저, weak에 대해서 알아보겠습니다. weak 참조는 메모리의 retain count를 증가시키지 않고 객체를 저장합니다. retain count를 증가시키지 않기 때문에 순환참조 문제를 방지할 수 있습니다.
참조한 메모리가 해제되었을 때는 ARC에 의해 자동적으로 nil로 초기화됩니다. 따라서, dangling pointer가 되는 것을 걱정하지 않아도 됩니다.
사용법은 변수 키워드 앞에 weak를 붙이면 됩니다. weak는 nil로 초기화될 수 있기 때문에 Optional type만 가능합니다. 당연히 let이 아닌 var로 선언해야합니다.

weak var reference: Optional type

weak 참조는 어디에서 사용할 수 있을 가요? 바로 순환 참조가 발생할 수 있는 참조나 delegate, closure에서 사용할 수 있습니다.
class Account {
    var bank: Bank?
		    
    deinit {
        print("deinit Accout")
    }
}

class Bank {
    var account: Account?

    deinit {
        print("deinit Bank")
    }
}

var account: Account? = Account()   // account retainCount: 1
var bank: Bank? = Bank()            // bank retainCount: 1

account?.bank = bank                // bank retainCount: 2
bank?.account = account             // account retainCount: 2

account = nil                       // account retainCount: 1
bank = nil                          // bank retainCount: 1

// 실행결과
// 아무것도 출력되지 않음.

위 코드는 순환참조가 발생하는 코드입니다. account 객체와 bank객체가 서로 참조하고 있기 때문에 순환참조가 발생합니다. retain count를 확인하면 순환참조가 발생했을 때 왜 메모리 누수가 발생하는 지 이유를 알 수 있습니다.
account 객체는 최초 생성이 되면 retain count는 1이 됩니다. bank객체도 마찬가지로 생성되면 retain count가 1이 됩니다. account객체가 bank객체를 bank property에 저장하면 bank객체의 retain count는 2가 되며 bank 객체가 account를 property에 저장하면 account객체도 retain count가 2가 됩니다.
account 객체를 해제하기 위해서 nil로 초기화하더라도 retain count가 1이 되기 때문에 account 객체는 해제되지 않습니다. bank객체도 nil로 초기화하면 retain count는 1이 되며 마찬가지로 메모리 해제가 되지 않습니다.
account객체와 bank객체에 접근 가능한 변수를 모두 nil로 초기화했기 때문에 객체에 접근할 수 있는 방법이 없습니다. 이렇게 메모리에 접근할 수 없는 영역이 발생하면 메모리 누수가 발생했다고 합니다.

위 코드의 순환참조를 해결하는 방법은 2가지가 있습니다. 직접 순환참조의 고리를 끊어주는 방법과 property를 weak으로 선언하는 방법입니다.

  • 순환참조 끊기

    var account: Account? = Account()
    var bank: Bank? = Bank()
    
    account?.bank = bank
    bank?.account = account
    
    account?.bank = nil		// 순환 참조 고리 끊기
    account = nil
    bank = nil
    
    // 실행결과
    // deinit Bank
    // deinit Accout
    

    account객체를 해제하기 전에 먼저 property에 저장하고 있는 bank객체를 해제하는 방법입니다. account의 bank를 nil로 설정하면 bank의 retain count가 1이 되며 account를 nil로 설정하면 account객체의 retain count가 1이 됩니다. bank를 nil로 설정하면 bank객체의 retain count는 0이 되고 이때 메모리 해제가 이루어집니다. 메로리 해제가 될 때 property도 함께 해제 되기 때문에 account객체의 retain count가 0이 되며 account객체도 메모리 해제가 되어 순환 참조 문제가 해결됩니다.

  • weak으로 선언

    class Bank {
        weak var account: Account?		// weak 추가
    
        deinit {
            print("deinit Bank")
        }
    }
    
    // 실행결과
    // deinit Account
    // deinit Bank
    

    Bank 객체의 account property를 weak으로 선언했습니다. 다시 account객체와 bank객체의 retain count를 계산해 보겠습니다. 처음 두 객체가 생성되면 둘다 retain count가 1이 됩니다. account 객체의 bank property에 bank객체를 저장하면 bank객체의 retain count는 2가 됩니다.
    bank객체의 weak account property에 account 객체를 저장하면 account 객체의 retain count는 증가하지 않고 그대로 1입니다.
    account를 nil로 설정하면 account 객체의 retain count는 0이 되고 메모리가 해제됩니다. 해제될 때 property도 함께 해제되기 때문에 bank객체의 retain count가 1이됩니다. bank를 nil로 설정하면 bank객체는 retain count가 0이 되고 메모리 해제가 됩니다. weak을 사용해서 순환참조 문제가 해결되었습니다.


closure에서 weak 사용

closure에서 weak을 사용할 때는 capture list에 작성해서 사용합니다.

var closure = { [weak self] in
    print(self?.name as Any)
}

closure에서 weak self를 사용할 때 optional binding으로 사용할 경우 편하게 self를 사용할 수 있습니다.
만약 아래 코드 처럼 사용 중에 참조한 self가 해제되면 어떻게 될가요?

var closure = { [weak self] in
 guard let strongSelf = self else { return }
    print(strongSelf.name)
    print(strongSelf.address)
    print(strongSelf.email)
}

weak 참조는 참조한 객체가 해제되는 시점에 nil로 변경되기 때문에 문제가 발생할 것으로 예상됩니다. 하지만 실제 closure를 실행해 보면 중간에 참조한 객체가 해제되도 아무 문제없이 동작합니다. 그 이유는 optional binding해서 새로운 변수에 참조한 객체를 저장하면 참조한 객체는 retain count가 1증가합니다. 참조한 객체가 외부에서 해제되어도 메모리는 해제가 되지 않고 closure실행이 끝난 시점에 실제 메모리 해제가 되기 때문에 weak 객체를 optional binding해서 사용해도 문제가 없습니다.



unowned 참조

unowned 참조는 weak 참조와 마찬가지로 retain count를 증가시키지 않고 메모리를 저장하며 순환참조 문제를 발생시키지 않습니다. weak와 다른 점은 참조한 객체가 메모리 해제되었을 때 ARC에 의해 nil로 값이 변경되지 않습니다. 즉, unowned 참조는 값이 항상 존재해야 합니다. 이 말은 unowned 참조의 생명 주기가 참조한 객체와 같거나 참조한 객체의 생명주기가 더 길때 사용할 수 있습니다. 만약 unowned가 참조된 상태에서 참조한 객체한 해제된다면 unonwed는 dangling pointer가 되며 그 unowned를 사용하면 runtime 오류가 발생합니다.
선언 방법은 unowned는 변수 키워드 앞에 붙여서 사용하며 optional type으로 선언할 필요는 없습니다.

unowned let reference

optional type으로도 선언은 가능하지만 중간에 참조한 객체가 해제되면 optional chaining으로 접근하더라도 runtime 오류가 발생합니다.


Tip: 생명주기가 같고 서로 상호작용을 해야하는 두 객체가 있는 상황

만약 자신의 초기화 함수에서 property를 생성하고 그 생성한 객체와 상호 참조해야할 경우 implicitly unwrapped optional과 unowned를 함께 사용해서 구현할 수 있습니다.

class Wallet {
    let ownerName: String
    var money: Money!
    
    init() {
        ownerName = "john"
        money = Money(money: 100, wallet:self)
    }
}

class Money {
    var money: Int
    unowned let wallet: Wallet
    
    init(money: Int,  wallet: Wallet) {
        self.money = money
        self.wallet = wallet
    }
}

위 코드에서 wallet객체를 생성할 때 money객체를 만들고 money객체는 wallet객체를 알아야 합니다. wallet의 초기화 1단계가 완료되기전까지 self를 사용할 수 없기 때문에 money를 생성할 수 없습니다. 이때, implicitly unwrapped optional이 해결책이 될 수 있습니다. wallet의 초기화 1단계에서 money property는 nil로 초기화되고 초기화 2단계에서 self를 사용할 수 있기 때문에 self를 money생성자에 전달해서 money객체를 생성할 수 있습니다.



주의사항: 재참조로 인한 문제

weak나 unowned로 참조한 객체를 다시 다른 변수에 저장할 경우 참조한 객체의 retain count가 증가해서 원하지 않는 문제를 야기할 수 있습니다.

  • 순환참조 재 발생
    weak나 unowned참조를 다시 다른 변수에 참조시켜면 순환 참조가 발생할 수 있습니다.

    class RetainCycle {
        var name = "name"
        var closeure: ( ()->() )?
        var reference: RetainCycle?
        
        deinit {
            print("deinit RetainCycle")
        }
        
        func setClosure() {
            closeure = { [unowned self] in
                print(self.name)
                self.reference = self	// 순환 참조를 야기시킴
            }
            closeure?()
        }
    }
    
    var test: RetainCycle? = RetainCycle()
    test?.setClosure()
    test = nil
    
    // 실행결과
    // name
    

    “deinit RetainCycle” 의 출력을 예상하였으나 실제 결과는 출력되지 않습니다. unowned참조를 다시 다른 변수에 참조시켜서 순환 참조가 발생하였습니다.


  • 의도치 않은 메모리 해제 지연
    의도한대로 바로 메모리 해제가 되지 않을 수 있습니다. weak으로 참조한 객체를 다시 다른 곳에 저장하면 원래 객체를 다 사용한 후 해제를 해도 메모리 해제가 되지 않아 원래 의도한 동작이 안될 수도 있습니다.

    class A {
        static let testNotificationName = Notification.Name.init(rawValue: "testNotificationName")
        init() {
            NotificationCenter.default.addObserver(self, selector: #selector(receiveNotification), name: A.testNotificationName, object: nil)
        }
     
        deinit {
            NotificationCenter.default.removeObserver(self, name: A.testNotificationName, object: nil)
        }
     
        @objc func receiveNotification() {
            print("receiveNotification")
        }
    }
    
    class B {
        weak var a: A?
        weak var c: C?
     
        func execute() {
            c?.a = a
        }
    }
    
    class C {
        var a: A?
    }
    
    
    var a: A? = A()
    var b: B? = B()
    var c: C? = C()
    b?.a = a
    b?.c = c
    b?.execute()
    
    a = nil
    NotificationCenter.default.post(name: A.testNotificationName, object: nil)
    
    // 실행결과
    // receiveNotification
    

    작성자는 a를 nil로 초기화했기때문에 더이상 notification을 받지 않을 것을 기대할 것이지만 실제 결과는 notification을 받고 있습니다.

따라서, 메모리를 미소유한 상태에서 다른 변수에 할당하는 구현은 삼가해야합니다.


마지막으로 weak와 unowned를 다시한번 정리하겠습니다.

weak와 unowned를 정리하면 아래 표와 같습니다.

구분 weak unowned
언제
사용?
참조한 객체가 실제 사용할 때 해제될지도 모를 때 사용. 보통 짧은 주기로 참조할 때 사용 참조한 객체와 생명주기가 같거나 참조한 객체가 더 길때 사용
순환참조
문제
발생하지 않음 발생하지 않음
참조한 객체가 해제되었을 때 nil로 값이 변경 dangling pointer가 됨


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

댓글

이 블로그의 인기 게시물

dismiss에 대해서 알아봅시다

1. Framework: framework란?

3. Framework: weak linking