ARC는 Automatic Reference Counting으로 참조타입의 참조 횟수를 자동으로 카운팅 되며 참조 수가 0이 되면 메모리에서 자동으로 해제하여 메모리 사용량을 관리합니다.

자동으로 참조횟수를 트래킹 하지만, 강한 참조 사이클 같은 경우에는 각 객체가 소멸해도 강한 참조가 남아있어 메모리에 계속 유지되어 버리는 메모리 누수 현상이 발생하곤 합니다.

그래서 메모리 누수 방지를 위해 weak, unowned 키워드를 종종 사용하곤 했습니다.

 

 

강한 참조 사이클 예시

// 서로 참조하는 클래스 두개 생성
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") }
}

// 인스턴스 생성 (Person의 강한참조: 1, Apartment의 강한참조: 1)
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "")

// person.apartment는 Apartment 클래스를 강한참조
// apartment.tenant는 Person 클래스를 강한참조
// (Person의 강한참조: 2, Apartment의 강한참조: 2)
person?.apartment = apartment
apartment?.tenant = person

// person과 apartment 소멸하여 강한참조가 1 줄어들지만, 서로 참조하는 카운팅은 남아있음
// 메모리에 계속 유지되는 Person클래스와 Apartment클래스는 어떤 방법으로도 접근할 수 없음
// (Person의 강한참조: 1, Apartment의 강한참조: 1)
person = nil
apartment = nil

 

 

 

weak? unowned?

강한참조 사이클 발생을 방지하기 위해 두 가지 키워드 중 하나를 선택하여 사용할 수 있습니다.

결과적으로 메모리 누수를 피하기 위해 사용되지만 두 키워드의 차이점은 있습니다.

 

weak: 옵셔널 타입으로 명시하며, 참조하는 객체가 소멸되면, 해당 값은 nil이 됩니다.

// 서로 참조하는 클래스 두개 생성
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") }
}

// 인스턴스 생성 (Person의 강한참조: 1, Apartment의 강한참조: 1)
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "")

// person.apartment는 Apartment 클래스를 강한참조
// apartment.tenant는 Person 클래스를 약한참조
// (Person의 강한참조: 1, Apartment의 강한참조: 2)
person?.apartment = apartment
apartment?.tenant = person

// person객체가 소멸되면 apartment.tenant가 약한참조로 nil이 된다.
// (Person의 강한참조: 0, Apartment의 강한참조: 0)
person = nil
apartment = nil

 

 

 

unowned: 옵셔널 형식이 아니며, 참조하는 클래스보다 수명이 같거나 긴 경우에만 사용합니다. Swift5 업데이트 이후로 unowned도 옵셔널 형식을 가질 수 있지만, 참조하는 클래스가 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?

// (Customer 강한참조: 1, CreditCard 미소유 참조)
// CreditCard의 수명은 Customer과 같기때문에 미소유 참조를 하였다.
// 만약 CreditCard 인스턴스를 생성하고, Customer가 nil일때 CreditCard.customer을 접근하면 앱 크래시 오류가 발생한다
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

 

서로 참조하는 객체의 수명이나 기획의도에 따라 키워드를 선택할 수 있다.

 

 

모든 서로 참조는 weak 키워드와 함께 쓰면 되지않을까?

unowned는 치명적인 오류를 발생하기 때문에 사용하지 않고 모든 서로참조를 weak 키워드와 함께 사용하면 되는 거 아닌가? 생각이 들었다. 기본적으로 weak는 옵셔널 형식의 약한 참조이다. 이에 따라 접근하기 위해 옵셔널 체이닝을 통해 접근하며, 언래핑이 필요하다.

그렇기 때문에 사용함에 있어서 값이 nil이 아님을 항상 증명해야 하고, 참조하는 객체가 메모리에서 내려가면 nil이 되므로 사이드 이펙트 발생 가능성 또한 존재하게 된다.

객체가 nil이 되지 않는 경우에는 weak보다 unowned 키워드가 더 적절하다.

 

 

 

둘 다 쓸 수 있는 상황에선 단순 취향 차이로 사용될 수 있을까?

두 개의 키워드는 사용목적은 같지만 성능면에서 차이가 드러납니다.

그전에 직접 앱을 돌려보면서 reference count를 알아봅시다.

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

먼저 두 개의 서로참조 클래스를 만들었습니다.

이를 lldb의 refCount를 확인하는 명령어를 통해 참조 카운트가 어떻게 진행되는지 알아보겠습니다.

명령어는 아래와 같습니다.

language swift refcount (클래스 변수명)

 

 

 

첫 번째로는 인스턴스 생성만 구현된 경우입니다.

var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "unit")

중단을 통해 참조카운팅을 확인해 보니 세 가지 유형의 참조가 카운팅되는걸 볼 수 있었습니다.

ARC를 공부했을 때 Strong Count만 세면 되는 줄 알았는데 실제로는 세가지 유형이 존재하는 걸 알 수 있었습니다.

근데 왜 인스턴스만 생성하고 서로 참조하지 않고 있는데 strong count = 3이 될까요?

-> lldb에서 해당 참조카운팅을 확인하기 위해 참조하고 일시적으로 메모리에 남겨두기 때문에 strong count = 3이 됩니다. 실제로는 그렇지 않습니다.

 

 

두 번째로는 서로 참조를 구현한 경우입니다.

person?.apartment = apartment
apartment?.tenant = person

서로 참조하니 strong 카운트가 1 증가했습니다.

바로 person변수와 aparment에 nil을 주입시키겠습니다.

 

 

세 번째로 nil을 할당한 경우

person = nil
apartment = nil

변수에 nil을 대입하니 lldb에서 더 이상 접근할 수 없었습니다. 대신 Debug Memory Graph를 통해 메모리가 누수되었다는 것을 확인할 수 있습니다.

 

위 카운팅처럼 weak, unowned도 모두 카운팅이 되고 있었습니다.

- strong: 객체에 대한 강한 참조의 개수를 셉니다. 강한 참조의 카운트가 0이 되면 deinit이 호출됩니다. 이 시점에서 unowned 접근은 앱크래시, weak 접근은 nil을 리턴합니다.

- unowned: unowned의 참조 개수를 셉니다. 강한 참조가 존재하면 +1이 추가됩니다. deinit이 완료되면 추가된 1이 차감됩니다. unowned가 0이 되면 최종적으로 메모리에서 해제합니다.

- weak: weak의 참조 개수를 셉니다. unowned 참조가 존재하면 +1이 추가됩니다. 객체가 메모리 할당 해제가 되면 추가된 1이 차감됩니다.

 

Side Table

추가적인 정보가 여기서 나오는데, Side Table은 Swift4.0에서부터 도입되었습니다.

객체 인스턴스가 생성됐을 때, ARC 카운팅과 함께 Side Table이 생성됩니다.

이는 객체의 간단한 정보들을 담고 있습니다. (참조 주소와 같은)

Side Table은 weak/unowned참조가 존재할 때 생성되고, 최종적으로 weak카운트가 0이 될 때 소멸됩니다.

 

weak 접근은 사이드 테이블을 통해 객체 참조 주소에 접근하게 됩니다. 이는 간접접근이라 볼 수 있습니다.

unowned은 별도로 사이드테이블을 통한 것이 아닌 직접 접근하게 됩니다.

weak와 unowned의 성능차이는 눈에 띌 정도로 나타나진 않지만, weak를 통한 접근은 추가적인 비용이 발생하기 때문에, 굳이 비교하자면 weak보다 unowned가 더 빠르다고 볼 수 있습니다.

 

 

 

메모리 누수 방지를 위해 strong 참조만 신경 썼지만, 더 많은 카운팅이 존재했고, weak와 unowned의 차이를 알 수 있었습니다!

'Swift > 문법' 카테고리의 다른 글

Swift의 타입들 Types  (0) 2025.03.26
후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12

+ Recent posts