ARC란 메모리 영역 중 힙 영역을 관리하는 것으로 Swift는 인스턴스, 클로저 등 레퍼런스 타입을 자동으로 힙에 할당한다. 지역 변수에 클래스 인스턴스를 생성하고 초기화 하면 지역 변수는 스택에 할당되고 실제 인스턴스는 힙에 할당되게 된다. 이렇게 되면 스택에 있는 지역 변수가 힙에 있는 인스턴스를 참조하고 있는 형태가 되고 지역 변수에는 실제 인스턴스의 주소값이 들어가 있게 된다.
이런 힙의 특징 중 하나는 사용하고 난 후에는 반드시 메모리 해제를 해줘야 한다는 점이다. 만약 스택에 있는 지역 변수가 함수가 종료된 후 사라지게 되면 남아있는 인스턴스는 어떻게 메모리 해제가 될까?
이 메모리 해제를 해주는 것이 바로 ARC이다.
ARC란 Automatic Reference Counting의 준말로 애플 공식 문서를 살펴보면
ARC란 클래스 인스턴스가 더 이상 필요하지 않을 때 메모리를 자동으로 해제하는 역할을 하는 것이다. 이 말은 힙에 손수 메모리를 해제 해주지 않아도 ARC가 자동으로 해제 해준다는 말이 된다. 한마디로 힙에 할당된 인스턴스의 메모리를 알아서 관리해주는 것이다.
ARC는 메모리의 참조 횟수(RC)를 계산하여 참조 횟수가 0이 되면 더 이상 사용하지 않는 메모리라 생각하여 해제하는 식으로 메모리를 관리한다. 이 인스턴스를 현재 누가 참조를 하고 있냐 아니냐를 숫자로 나타낸 것이라고 보면 된다. 만약 참조 횟수가 10이라면 이 인스턴스를 10군데에서 참조되고 있단 것이고 0이라면 참조하고 있는 곳이 없는 것이다.
참조 횟수가 +1되는 순간은 인스턴스의 주소값을 변수에 할당할 때이다. 두 가지 경우가 있는데 첫 번째는 인스턴스를 새로 생성할 때이다. 변수에 인스턴스를 생성하여 대입할 때 RC가 증가한다.
두 번째는 기존에 있는 인스턴스를 다른 변수에 대입할 때이다. 기존 인스턴스를 다른 변수에 대입하는 것 역시 그 변수가 인스턴스를 참조하게 되는 것이므로 RC가 증가한다.
참조 횟수가 -1되는 순간은 크게 3가지 경우가 있다.
첫 번째는 인스턴스를 참조하던 변수가 메모리에서 해제되었을 때이다. 예를 들어 인스턴스를 참조하던 지역 변수가 해당 함수가 종료되면서 스택에서 해제되었을 때 RC가 감소한다.
두 번째는 nil이 지정되었을 때이다. nil이 지정될 수 있는 옵셔널 타입의 변수인 경우 nil이 지정되었을 때 기존 인스턴스를 더 이상 참조하지 않으므로 RC가 줄어든다.
세 번째는 변수에 다른 값을 대입할 경우이다. 변수가 참조하고 있던 기존 인스턴스가 아닌 다른 인스턴스를 대입했을 경우 기존 인스턴스의 RC가 줄어든다.
strong이란 강한 참조로 위의 예 중 RC가 증가하는 경우(인스턴스의 주소값을 변수에 할당할 때)를 강한 참조라고 한다. 이 강한 참조로 인해서 ARC의 단점이 발생할 수 있는데 이는 영구적으로 메모리가 해제되지 않을 수 있다는 점이다.
순환 참조란 예를 들어,
class Man {
var name: String
var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man Deinit!") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman Deinit!") }
}
이러한 Man과 Woman이라는 클래스가 있다. 눈여겨 봐야할 점은 Man 클래스에는 Woman 타입의 프로퍼티가 있고 Woman 클래스에는 Man 타입의 프로퍼티가 있는 점이다.
var chelosu: Man? = .init(name: "철수")
var yeonghee: Woman? = .init(name: "영희")
이렇게 두 인스턴스를 생성했을 때 메모리에는
다음과 같이 할당되게 된다. 이 상태에서
chelosu?.girlfriend = yeonghee
yeonghee?.boyfriend = chelosu
이렇게 할당하게 되면 메모리는
이렇게 변하게 된다. girlFriend, boyFriend 프로퍼티는 strong이기 때문에 RC가 증가한 것이다.
이런식으로 두 객체가 서로서로를 참조하고 있는 형태를 강한 순환 참조라고 한다. 이 순환 참조의 문제점은 만약
chelosu = nil
yeonghee = nil
둘 다 nil을 지정했을 경우에 발생한다. 원하는 시나리오는 RC가 0이 되어 두 개의 인스턴스가 힙에서 메모리 해제되어야 하는 것이다. 하지만 문제는 이럴 경우에 메모리 해제가 되지 않고 메모리 누수가 발생한다는 것이다.
그 이유는
nil을 지정하여 RC가 줄어들어도 순환 참조로 인해서 증가했던 RC값이 1 남아있기 때문이다. 서로가 서로를 참조하기 때문에 RC가 0이 되지 못하고 메모리 누수가 발생하게 된다. 이를 위해서 weak와 unowned가 필요하다.
weak란 strong의 반대로 약한 참조를 뜻한다. 약한 참조란 인스턴스를 참조할 시 RC를 증가시키지 않고 참조하던 인스턴스가 메모리에서 해제된 경우 자동으로 nil이 할당되어 메모리가 해제된다. nil이 할당되어야 하기 때문에 weak는 옵셔널 타입의 변수여야 한다. 위의 강한 순환 참조를 해결하기 위해서 한 쪽을 weak로 선언해 줄 수 있다.
class Man {
var name: String
weak var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man Deinit!") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman Deinit!") }
}
순환 참조를 일으키는 프로퍼티 앞에 weak를 선언하면 된다. 이런 경우 메모리는
이렇게 되고 girlfriend 프로퍼티가 weak이기 때문에 Woman Instance의 RC는 변하지 않는다.
이 상태에서 다시 두 변수에 nil을 지정할 경우
어떤 인스턴스(Woman Instance)의 프로퍼티(boyfriend)가 다른 인스턴스(Man Instance)를 가리키고 있을 때, 그 프로퍼티(boyfriend)가 속한 인스턴스(Woman Instance)가 메모리에서 해제되면 그 프로퍼티(boyfriend)가 가리키고 있던 인스턴스(Man Instance)의 RC가 -1 감소한다)
이렇게 순환 참조이지만 weak로 선언되어 RC 값을 올리지 않는 것을 약한 순환 참조라고 한다.