Overview

Class 타입의 값을 할당할 때는 struct, enum(열거형), literal과는 달리 참조 타입의 값 전달이 일어납니다. 참조 타입은 하나의 클래스 인스턴스에 여러 개의 객체들이 공통으로 접근하기 때문에 인스턴스가 메모리에서 해제되는 적절한 시점을 정하는 것이 중요합니다. 인스턴스를 참조하는 다른 객체가 아직 사용중인데 메모리에서 해제된다면 잘못된 메모리 접근 오류가 발생할 수도 있고, 그 인스턴스를 참조하는 객체가 없는데도 계속 메모리에 남아있다면 한정적인 메모리 자원을 낭비하게 될 수도 있습니다. 그래서 인스턴스를 생성하면 프로그래머는 적절한 시점에서 명시적으로 메모리에서 해제시켜야 합니다. Swift에서는 인스턴스의 메모리 해제를 프로그래머 대신 해 주는 ARC 기법을 사용하여 프로그래머를 메모리 관리로부터 조금은 자유롭게 만들어 주었습니다.

ARC(Automatic Reference Counting)

ARC는 Swift의 자동 메모리 관리 기법입니다. 인스턴스의 참조 횟수를 추적하여 인스턴스가 더 이상 참조되지 않을 때 해당 인스턴스를 메모리에서 자동으로 해제시켜 줍니다. 즉, 인스턴스가 참조된 횟수를 세다가(counting) 참조 횟수가 0이 되면 해당 인스턴스를 메모리에서 해제시킵니다.

일반적으로 인스턴스의 참조 횟수는 인스턴스를 생성하여 객체에 할당하는 시점에 증가하여 인스턴스가 할당된 객체가 소멸하는 시점에 감소합니다. 하지만, 아래에서 보게 될 서로 다른 인스턴스가 상호 참조하면서 발생할 수 있는 순환 참조같은 문제들 때문에 참조 횟수를 카운팅하지 말아야 하는 경우도 생길 수 있습니다. Swift에는 참조 횟수 카운팅 여부를 결정하기 위한 세 가지 옵션이 존재합니다.

강한 참조(Strong Reference)

강한 참조는 프로퍼티, 변수, 상수 등에 인스턴스를 할당할 때 기본으로 설정되는 옵션으로, strong 키워드를 이용해 해당 프로퍼티가 참조 카운트를 하는 변수임을 컴파일러에 알려줍니다.

Swift에서는 아무런 키워드도 붙이지 않으면 자동으로 strong 옵션으로 인식하지만, Objective-C에서는 property 선언 시 strong을 명시해줍니다.

Swift

// Equal to 'strong var button: UIButton'
// Reference count + 1
var button: UIButton = button = UIButton(type: .system)                

Objective-C

@property (nonatomic, strong) UIButton *button;
_button = [UIButton buttonWithType:UIButtonTypeSystem];    // Count + 1

참조 카운팅은 강한 참조를 할 때 발생합니다. 객체에 인스턴스가 할당되면 해당 인스턴스의 참조 횟수가 증가하고, 객체의 생명주기가 끝나면 참조 횟수가 감소합니다. 객체의 생명 주기가 끝나는 시점은 다음과 같습니다.

  1. 객체가 프로퍼티로 선언되었을 때, 해당 프로퍼티를 갖고있는 클래스의 인스턴스가 메모리에서 해제되는 경우
  2. 객체가 함수 및 메서드 내에서 변수 또는 상수로 선언되었을 때, 객체가 선언된 코드 블럭의 실행이 끝나는 경우
  3. 객체에 nil이 할당되는 경우

인스턴스가 할당된 객체(프로퍼티, 변수, 상수 등)의 생명 주기가 끝나면, 해당 객체가 참조하는 인스턴스가 메모리에서 해제되면서 클래스의 소멸자(deinitializer)가 호출됩니다.

순환 참조의 문제

강한 참조를 사용하다 보면 문제가 생길 때가 있습니다. 대표적으로 인스턴스가 서로를 강한 참조하는 경우 인스턴스가 메모리에서 영원히 해제되지 않는 상황이 발생하는데, 이를 강한참조 순환 또는 순환 참조 문제라고 합니다. 실제로 순환참조가 발생하는 경우를 살펴보겠습니다.

class Person {
  let name: String
  var car: Car?

  init(name: String) {
    self.name = name
    print("\(name) is being initialized")
  }

  deinit {
    print("\(name) is being deinitialized")
  }
}

class Car {
  let model: String
  var owner: Person?

  init(model: String) {
    self.model = model
    print("\(model) is being initialized")
  }

  deinit {
    print("\(model) is being deinitialized")
  }
}

Person 클래스는 자동차를 산 사람도 있고 아직 없는 사람도 있을 수 있으므로, optional Car 타입의 프로퍼티를 가집니다. Car 클래스는 갓 출고된 차라면 아직 주인이 없을 것이고 누군가의 명의로 등록될 수도 있으므로 optional Person 타입의 프로퍼티를 가집니다.

// Person, Car 객체 선언
var cskim: Person?
var tivoli: Car?

// Person 인스턴스 생성, Person ref = 1
cskim = Person(name: "cskim")
// Car 인스턴스 생성, Car ref = 1
tivoli = Car(model: "tivoli")

// Car 인스턴스 참조, Car ref = 2
cskim?.car = tivoli
// Person 인스턴스 참조, Person ref = 2
tivoli?.owner = cskim

// Print
// cskim is being initialized
// tivoli is being initialized

Person과 Car 클래스의 인스턴스를 생성해서 cskimtivoli 객체에 각각 할당합니다. 객체에 할당되는 시점에서 강한 참조를 하여 참조 횟수가 각각 1 증가합니다. 또, carowner 프로퍼티에 각각 cskimtivoli 객체를 할당하며 강한 참조하므로 참조 횟수가 각각 1 증가합니다. 이렇게 Person 인스턴스는 car 프로퍼티로 Car 인스턴스를 강한 참조하고 Car 인스턴스는 owner 프로퍼티로 Person 인스턴스를 강한 참조하게 되었습니다.

// Person 참조 해제, Person ref = 1
cskim = nil
// Person 참조 해제, Person ref = 0
// Person 인스턴스가 메모리에서 해제, Car ref = 1
tivoli.owner = nil
// Car 참조 해제, Car ref = 0
tivoli = nil

// Print
// cskim is being initialized
// tivoli is being initialized
// cskim is being deinitialized
// tivoli is being deinitialized

여기서 cskimnil을 할당하면 Person의 참조 횟수가 1로 줄어듭니다. cskim.car 프로퍼티에 접근할 방법은 없어졌지만, 만약 tivoli.ownernil을 할당한다면 Person 인스턴스의 참조 횟수가 0이 되어 메모리에서 해제됩니다. 이 때, Person 클래스의 car 프로퍼티가 강한 참조하던 Car 인스턴스의 참조 횟수도 함께 감소합니다. 마지막으로 tivoilnil을 할당하면 Car 인스턴스의 참조 횟수가 0이 되어 메모리에서 해제됩니다.

그런데 우리는 cskimtivoli에 각각 nil을 할당하기만 하면 자연스럽게 메모리가 정리되길 원합니다(적어도 저는 이게 더 직관적이고 자연스러운 것 같습니다).

// Person 참조 해제, Person ref = 1
cskim = nil
// Car 참조 해제, Car ref = 1
tivoli = nil

// Print(Deinitializer isn't called)
// cskim is being initialized
// tivoli is being initialized

하지만, 이렇게 하면 Person과 Car 인스턴스는 영원히 메모리에서 해제되지 않습니다. 참조 횟수가 0이 아니기 때문에 인스턴스가 메모리에서 해제되지 않고, 때문에 carowner 프로퍼티에 자동으로 nil이 할당되지 않습니다. cskimtivoli 객체를 메모리에서 해제시켰기 때문에 carowner 프로퍼티에 접근하여 nil을 할당할 수도 없습니다. 약한 참조미소유 참조라는 개념을 이용해 좀 더 깔끔하고 자연스러운 코드로 이 문제를 해결해 보겠습니다.

약한 참조(Weak Reference)

약한 참조는 인스턴스의 참조 횟수를 증가시키지 않도록 하며, weak 키워드를 사용하여 해당 객체가 인스턴스를 약한 참조함을 컴파일러에 알려줍니다.

weak var button: UIButton?        
button = UIButton(type: .system)    // Don't counting

약한 참조는 참조 횟수를 증가시키지 않기 때문에 아직 인스턴스를 참조하는 객체가 있어도 참조 횟수가 0이 된다면 해당 인스턴스가 메모리에서 해제됩니다. 그렇기 때문에 해당 객체가 참조하는 인스턴스가 언제든 메모리에서 해제될 수 있음을 예상하고 있어야 합니다.

약한 참조하는 객체는 인스턴스가 메모리에서 해제되면 자동으로 nil이 할당될 수 있어야 하기 때문에, 약한 참조하는 객체는 옵셔널 변수로 선언되어야 합니다.

약한 참조를 사용하여 다음과 같이 순환 참조 문제를 해결할 수 있습니다.

class Car {
  let model: String
  weak var owner: Person?    // Don't counting reference

  init(model: String) {
    self.model = model
  }

  deinit {
    print("\(model) is being deinitialized")
  }
}

// Person reference = 1
let cskim: Person? = Person(name: "cskim")
// Car reference = 1
let tivoli: Car? = Car(model: "tivoli")

// Car reference = 2
cskim.car = tivoli
// Person reference = 1
tivoli.owner = cskim

// Person reference = 0, Car reference = 1
cskim = nil
// Car reference = 0
tivoli = nil

// Print
// cskim is being initialized
// tivoli is being initialized
// cskim is being deinitialized
// tivoli is being deinitialized

ownercskim이 할당되어도 Person 인스턴스를 참조하지 않기 때문에 cskimnil이 할당되면 Person 인스턴스가 메모리에서 해제됩니다. 동시에 car 프로퍼티가 강한 참조하던 Car 인스턴스의 참조 횟수도 1 감소하게 되고, tivolinil을 할당하면 Car 인스턴스의 참조 횟수도 0이 되어 메모리에서 해제됩니다. Car 인스턴스가 메모리에서 해제되면서 owner 프로퍼티는 자동으로 nil이 할당됩니다.

미소유 참조(Unowned Reference)

미소유 참조는 약한 참조와 마찬가지로 참조 횟수를 증가시키지 않도록 하며, unowned 키워드를 사용합니다. 약한 참조와의 차이점은 자동으로 nil을 할당하는가에 있습니다.

unowned var button: UIButton                
button = UIButton(type: .system)    // Don't counting

미소유 참조는 약한 참조와 달리 참조하고 있는 인스턴스가 메모리에서 해제되더라도 자동으로 nil을 할당하지 않습니다. 그렇기 때문에 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 확신이 있을 때만 사용해야 합니다. 이미 해제된 인스턴스의 메모리 주소에 접근하려고 하면 런타임 오류가 발생할 수 있습니다.

미소유 참조는 앞에서 본 순환 참조 같은 문제가 발생했을 때 해당 변수가 값을 반드시 가져야 하는 경우에 사용할 수 있습니다. 즉, 강한 참조 문제를 해결할 때 약한 참조를 사용할 수 없을 때 사용합니다. 신용 카드의 경우를 생각해 보면, 사람은 신용카드를 갖지 않을 수도 있지만 신용 카드는 반드시 소유자가 있어야 합니다. 따라서 신용 카드의 소유주는 옵셔널로 선언하지 않고, 순환 참조의 문제를 피하기 위해 강한 참조를 사용해서는 안됩니다.

class Person {
  let name: String
  var card: CreditCard?

  init(name: String) {
    self.name = name
    print("\(name) is being initialized")
  }

  deinit {
    print("\(name) is being deinitialized")
  }
}

class CreditCard {
  let number: UInt
  unowned let owner: Person

  init(number: UInt, owner: Person) {
    self.number = number
    self.owner = owner
    print("\(number), \(owner.name)" is being initialized)
  }

  deinit {
    print("\(number), \(owner.name)" is being deinitialized)
  }
}

CreditCard 클래스의 owner 프로퍼티는 소유주가 반드시 있어야하기에, non-optional 타입의 미소유 참조하는 상수로 선언되었습니다.

// Person reference = 1
var cskim: Person? = Person(name: "cskim")

if let person = cskim {
  // CreditCard reference = 1, Person reference = 1
  person.card = CreditCard(number: 1234, owner: person)
}

// Person reference = 0
// CreditCard를 강한 참조하는 'card' 프로퍼티의 참조 해제, Card reference = 0
cskim = nil

Card 클래스의 owner 프로퍼티가 Person을 미소유 참조하므로, CreditCard 인스턴스가 생성되면서 owner 프로퍼티가 Person을 참조하지만 참조 카운트는 발생하지 않습니다. cskimnil을 할당하면 Person 인스턴스의 참조 횟수가 0이 되어 메모리에서 해제되고, Person 인스턴스가 해제되면서 CreditCard를 강한 참조하던 card 프로퍼티의 참조가 해제되어 CreditCard 인스턴스도 메모리에서 해제됩니다.

Summary

  • 참조 카운트가 증가하는 경우
    1. 강한 참조하는 변수, 상수 등에 인스턴스가 할당될 때
    2. 강한 참조하는 프로퍼티에 인스턴스가 할당될 때
  • 참조 카운트가 감소하는 경우
    1. 강한 참조하는 프로퍼티, 변수 등에 nil을 할당할 때
    2. 강한 참조하는 프로퍼티의 생명 주기가 끝날 때(해당 프로퍼티를 멤버로 갖는 클래스의 인스턴스가 메모리에서 해제될 때)
    3. 강한 참조하는 변수, 상수 등의 생명 주기가 끝날 때(해당 변수 및 상수가 선언된 함수, 메서드, 코드 블럭의 실행이 끝날 때)
  • 강한 참조
    1. strong 키워드 선언
    2. 변수, 상수, 옵셔널 선언
  • 약한 참조
    1. weak 키워드 선언
    2. 옵셔널 변수에만 선언(참조하는 인스턴스가 메모리에서 해제될 때 nil 할당)
  • 미소유 참조
    1. unowned 키워드 선언
    2. 옵셔널 또는 암시적 추출 옵셔널 변수에 선언(nil이 할당될 수 없는 경우. 즉, 약한 참조를 사용할 수 없는 경우)