swift 기본문법 - 자동참조 카운팅, ARC(Automatic Reference Counting)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
참고씩이머릿속-자동참조카운팅


자동참조 카운팅, ARC(Automatic Reference Counting)

메모리 사용을 관리하고 추적하기 위해 사용

대부분의 경우 메모리 관리는 ARC가 알아서 하고, 우리는 메모리 관리를 신경쓸 필요가 없다.
ARC는 클래스 인스턴스가 더이상 필요하지 않을 때 자동으로 그 인스턴스가 사용하는 메모리를 제거한다. 참조 카운팅은 클래스의 인스턴스에만 적용되며 구조체와 열거형은 참조 타입이 아니라 값 타입이라 해당사항이 없다.

ARC 작동원리

  • 클래스의 새로운 인스턴스를 생성할 때 마다 ARC는 인스턴스의 정보를 저장하기 위해서 메모리를 할당한다.
  • 메모리에는 해당 인스턴스와 연결된 모든 저장 프로퍼티의 값과 함께 인스턴스의 타입에 대한 정보가 저장된다.
  • 인스턴스가 필요 없어질 때, ARC 는 메모리를 다른 목적으로 사용될 수 있게 하기 위해 그 인스턴스가 사용하는 메모리를 해제한다.

이렇게 하면 클래스 인스턴스가 더 이상 필요하지 않을때 메모리 공간을 차지하지 않도록한다.(효율성 업!)

ARC는 해당 인스턴스에 대한 참조가 적어도 하나 이상 있다면, 인스턴스를 해제하지 않으며 이를 가능하게 하려면 클래스 인스턴스를 프로퍼티나 변수, 상수에 할당할 때마다 강한 참조(strong reference)를 만들어야 한다 ( 변수가 인스턴스를 꽉 붙잡고 있다고 생각하면 될듯. )

class Person { // Person 클래스에는 이름과 초기화 상태를 나타내는 초기화 함수와 초기화 해제 함수가 있음
   let name: String
   init(name: String) {
       self.name = name
       print("\(name) is being initialized")
   }
   deinit { // 인스턴스가 메모리 해제될 때 메세지 출력하게 하는 deinit
       print("\(name) is being deinitialized")
   }
}
// Person 클래스 타입의 세 변수 옵셔널로 생성. ARC 의 상태 변화 알아보기 위함
var reference1: Person? // 옵셔널 타입이므로 현재는 nil 로 초기화
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed")// "John Appleseed is being initialized" 출력
// Person 인스턴스의 메모리가 생기는 시점
// referece1 에서 새로운 Person 인스턴스에 대한 강력한 참조가 생긴다
// 강한참조가 하나 이상 생겼기 때문에  ARC 는 이 인스턴스를 해제하지 않는다

reference2 = reference1
reference3 = reference1
// 같은 인스턴스 참조를 2, 3 에 모두 할당하면?
// 해당 인스턴스에 대한 강한 참조가 2개 더 생기는 것
// 그럼 현 시점에서는 Person 인스턴스에 대한 강한 참조는 3개 인 상태

reference1 = nil
reference2 = nil
// 2곳에 nil 할당해서 2개의 강한 참조를 끊으면?
// 아직 1개, 마지막 강한 참조가 남아있으므로 ARC 는 Person 인스턴스 메모리를 해제하지 않음
reference3 = nil // "John Appleseed is being deinitialized" 출력
// ARC 가 Person 인스턴스 메모리 해제시키는 시점
// 마지막 참조가 남아있는 reference3 에도 nil 할당하면, 인스턴스 안쓰는걸로 알고 ARC 가 메모리 없애버림

ARC와 GC의 차이

메모리 관리 기법 ARC GC
참조 카운팅 시점 컴파일 시 프로그램 동작 중
장점 컴파일 당시 이미 인스턴스의 해제 시점이 정해져 있어 인스턴스가 언제 메모리에서 해제될 지 예측 가능하다. 따라서 메모리 관리를 위한 시스템 자원을 추가할 필요가 없다 상호 참조 상황등의 복잡한 상황에서도 인스턴스를 해제할 수 있는 가능성이 더 높으며 특별히 규칙에 신경 쓸 필요가 없다.
단점 ARC의 작동 규칙을 모르고 사용하면 인스턴스가 영영 메모리에서 해제되지 않을 가능성이 있다 프로그램 동작 외의 메모리 감시를 위한 추가 자원이 필요해 한정적인 자원 환경에서 성능 저하가 발생할 수 있다. 더 나아가 명확한 규칙이 ㅇ벗기 때문에 인스턴스가 정확히 언제 메모리에서 해제될 지 에측이 어렵다.

강한 참조와 강한참조 순환 문제

인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어주는 것이 강한참조이다. 참조의 기본은 강한 참조이기 때문에 클래스 타입의 프로퍼티, 변수, 상수 등을 선언할 때 별도의 식별자를 명시하지 않으면 강한 참조가 된다.

class Person {
  let name: String
}

기본적으로 인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되는데, 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 강한 참조를 사용하면 참조 횟수가 +1 되며 강한 참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당해주면 그때 -1가 된다. 참조 횟수가 1이라도 남아있으면 메모리에서는 이를 해제하지 못하는데, 이렇게 강한 참조를 걸게 됨으로써 인스턴스가 메모리 해제되지 않아 계속된 메모리 누수를 만드는 경우가 있다. 그게 바로 강한 참조 순환 문제 이다.

class Person {
  let name: String

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

  var room: Room?

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

class Room {
  let number: String

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

  var host: Person?

  deinit {
    print("Room \(number) is being deinitialized")
  }
}

var zehye: Person? = Person(name: "zehye")  // Person의 인스턴스 참조: 1
var room: Room? = Room(number: "22")  // Room의 인스턴스 참조: 1

room?.host = zehye  // Person의 인스턴스 참조: 2
zehye?.room = room  // Room의 인스턴스 참조: 2

zehye = nil  // Person의 인스턴스 참조: 1
room = nil   // Room의 인스턴스 참조: 1

zehye와 room에 nil을 할당하게 됨으로써 더이상 zehye 변수가 참조하던 Person클래스의 인스턴스에 접근할 방법도 room변수가 참조하던 Room 클래스의 인스턴스에 접근할 방법이 사라지게 되었다. 참조 횟수가 0이 되지않는 한, ARC의 규칙대로라면 인스턴스를 메모리에서 해제시키지 않기 떄문에 이렇게 두 인스턴스 모두 참조 횟수가 1이 남은체 메모리에 좀비처럼 남아있게 된다.

이를 해결하기 위한 방법으로 약한참조미소유참조 가 있다.

약한 참조

약한 참조는 강한 참조와는 다르게 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다. 참조타입의 프로퍼티나 변수의 선언 앞에 weak 를 써주면 약한참조를 할 수 있다. 이는 참조횟수를 증가시키지 않기 떄문에 이스턴스를 강한 참조하던 프로퍼티나 변수에서 참조 횟수를 0으로 만들면 자신이 참조하던 인스턴스가 메모리에서 해제될 수 있다.

약한 참조는 상수에서 쓰일 수 없다. 자신이 참조하던 인스턴스가 메모리에서 해제된다면 nil이 할당될 수 있어야 하기 때문이다. 그래서 약한 참조를 할 때 자신의 값을 변경할 수 있는 변수로 선언해야하며 nil이 할당될 수 있어야 하기에 약한 참조는 항상 옵셔널이어야 한다. 즉 옵셔널 변수만이 약한참조를 할 수 있다.

class Room {
  let number: String

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

  weak var host: Person?

  deinit {
    print("Room \(number) is being deinitialized")
  }
}

var zehye: Person? = Person(name: "zehye")  // Person의 인스턴스 참조: 1
var room: Room? = Room(number: "22")  // Room의 인스턴스 참조: 1

room?.host = zehye  // Person의 인스턴스 참조: 1
zehye?.room = room  // Room의 인스턴스 참조: 2

zehye = nil  // Person의 인스턴스 참조: 0, Room의 인스턴스 참조: 1
// zehye is being deinitialized
room = nil   // Room의 인스턴스 참조: 0
// Room 22 is being deinitialized

미소유 참조

약한 참조와 마찬가지로 미소유 참조 또한 인스턴스의 참조 횟수를 증가시키지 않는다. 그러나 미소유 참조는 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 동작한다. 즉, 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 스스로 nil을 할당해주지 않는다는 것을 의미한다. 따라서 미소유참조를 하는 변수나 프로퍼티는 옵셔널이나 변수가 아니어도 된다. 그러나 미소유 참조를 하면서 메모리에서 해제된 인스턴스에 접근하려 한다면 잘못된 메모리 접근으로 런타임 오류가 발생해 프로세스가 강제로 종료된다. 따라서 미소유 참조는 참조하는 동안 해당 인스턴스가 메모리에서 해제되지 않을 것이라는 확신이 있을때만 사용해야 한다.

이러한 미소유참조는 참조타입의 변수나 프로퍼티 앞에 unowned 키워드를 사용해주면 된다.

class Person {
  let name: String

  // Person은 카드를 소지할 수도 소지하지 않을 수도 있다.
  // 카드를 한번 가진 후 잃어버리면 안되기에 강한참조로!
  var card: Card?
  init(name: String) {
    self.name = name
  }

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

class Card {
  let number: Int

  // 카드는 소유자가 분명히 존재해야한다.
  unowned let owner: Person
  init(number: UInt, owner: String) {
    self.number = number
    self.owner = owner
  }

  deinit { print("Card \(number) is being deinitialized")}
}

var zehye: Person? = Person(name: "zehye")  // Person 인스턴스 참조: 1

if let person: Person = zehye {
  // Card의 인스턴스 참조: 1
  person.card = Card(number: 11, owner: person)
  // Person 인스턴스 참조: 1
}

zehye = nil  // Person 인스턴스 참조: 0
// Card 인스턴스 참조: 0
// zehye is being deinitialized
// Card 11 is being deinitialized