Search
🔄

레퍼런스 카운드(Reference Count) 와 순환 참조(Reference Cycle)

Objective-C부터 Swift까지

iOS 개발을 시작하면 항상 따라오는 주제가 있다.
ARC가 나온 마당에 이게 뭐가 중요하냐고 할 수도 있겠지만 초급을 벗어나려면 꼭 알아두어야 하는 개념이다.
Objective-C(objc) 는 C를 기반으로 하고 있다. 이름 그대로 C에 Object(객체) 개념을 추가한 언어인데 C++과 비교되지만 시작부터 다른언어다. (하지만 XCode는 C++을 계속 지원해주고 있지)
아무튼 이런 태생으로 포인터 개념을 가지게 되었고 인스턴스를 생성할 때 마다 heap 메모리를 할당해준다.
이 후 사용이 종료된 메모리는 해제 해줘야 할 텐데 이 방식 중 하나가 Reference Count 다.
원리는 간단하다.
1.
객체를 선언해 인스턴스를 생성해주면 +1
2.
이 인스턴스가 더이상 필요하지 않다고 판단되면 -1
3.
이렇게 카운트가 변화하다 0이 되면 아무도 안쓰니 메모리에서 해제
그래서 초창기에 iOS 개발에서는 사용 후에 반드시 release를 해줘야만 했다.
UILabel *myName = [[UILabel alloc] init]; myName.text = @"Allen"; [view addSubView: myName]; [myName release];
Objective-C
복사
이 외에도 dealloc, retain, autorelease도 있다.
그 후 ARC(Auto Reference Counting) 이 나오자 별도의 작업 없이도
시스템이 알아서 카운트를 증가시키고 감소시키고 해제해주는 동작을 하게 되었다.
핵심은 ARC는 메모리 관리를 해주는 애가 아닌 레퍼런스 카운트를 관리해주는 애라는 것이다. (언뜻 같은 말 같지만;;)
여전히 레퍼런스 카운트는 개발자가 관리해줘야 하는 것들 중 하나다.
그럼 뭘 어떻게 관리해주냐는 얘기가 나올 때 또 자주 들리는 말이 있다.
순환참조 라는 것인데, 개념은 간단하다.
레퍼런스 카운트가 증감되며 메모리의 생성과 해제가 이루어지는데
이 레퍼런스 카운트가 줄어들지 못하고 남아 있게 된다면?
메모리는 계속 해제되지 못한 채 자리를 차지하고 있을 것이다. 이것이 누적되면 펑하고 폰이 폭발 앱이 종료된다.
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") } }
Swift
복사
이렇게 되면 각 클래스가 초기화 될 때 서로에 대한 참조 카운트를 증가시킬 것이다.
var john: Person? var unit4A: Apartment? // john은 Person으로 초기화 되며 +1 // Person은 Apartment를 가지게 되며 +1 john = Person(name: "John Appleseed") // unit4A은 Apartment로 + 1 // Apartment는 Person으로 +1 unit4A = Apartment(unit: "4A") john.apartment = unit4A unit4A.tenant = john
Swift
복사
여기서 john과 unit4A을 전부 nil로 바꿔준다면?
john = nil unit4A = nil // john은 Person이 해제되며 -1 // unit4A은 Apartment가 해제되며 -1 // Person과 Apartment가 가진 서로의 참조 카운트는???
Swift
복사
안사라진다. 그냥 메모리 릭이 된다.
이렇게 서로 레퍼런스 카운트를 가지게 되어 영원히 해제되지 않는 것을 레퍼런스 사이클, 순환참조라고 부른다.
그럼 이걸 어떻게 해결 하느냐?
설계에 따라 강한참조를 제거해주면 된다.
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으로 선언해줬다. weak var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
Swift
복사
john = nil
Swift
복사
unit4A = nil
Swift
복사
GC(Garbage Collection) 의 경우 weak으로 선언했더라도 메모리에 여유가 있다면 즉시 해제되지 않고 남겨둔다. (나중에 한번에 처리하려고) But, ARC에서는 즉시 해제된다.

클로저에서의 순환 참조

또 하나 순환참조가 자주 일어나는 곳이 있다. 바로 클로저를 사용하는 부분인데, 위 상황에 경우 특정 상황에서만 일어나는 반면 클로저에서의 순환참조는 공부 안한사람이면 바로 실수 하는 부분이기도 하다.
애플에서 제공하는 예시는 와닿지가 않는다. 잘 안쓰는 방식이라 그런거 같다.
솔직히 이 3가지 방식이 제일 많이 그리고 자주 쓰이는 방식 아닌가 싶다.
var text = "text" var name = "name"
Swift
복사
DispatchQueue.global(qos: .default).sync { // Do something DispatchQueue.main.async { // Do completion self.text = "text chagned" } }
Swift
복사
UIView.animate(withDuration: 0.1, animations: { // Do something }, completion: { _ in // Do completion self.name = "name changed" }) }
Swift
복사
func doSomething(then completionHandler: @escaping (() -> Void)) {} myClass.doSomething(then: { // Do completion self.text = "Do Something" })
Swift
복사
여기도 마찬가지로 클로저를 선언할 때 클로저에 인스턴스의 참조를 할당하기 때문에 순환참조가 발생한다.
클로저 내부에서 self 로 접근 할 경우 self 에 대한 강한 참조를 가지게 됩니다.
해결 방법은 간단
doSomething(then: { [unowned self] in // Do completion self.str = "unowned Do Something" }) doSomething { [weak self] in self?.str = "weak Do something" } doSomething { [weak self] in guard let strongSelf = self else { return } strongSelf.str = "weak Do something" }
Swift
복사
이렇게 하면 클로저에 self 가 참조 될 때 strong 대신 unowned / weak 으로 참조되어 해당 인스턴스가 해제될 때 함께 해제된다.
궁금한 점은 댓글~
참고 :