[Swift] ARC-Strong Reference Cycle(강한참조사이클; 순환참조)

MSDev·2022년 3월 8일
0

저번 ARC 기본 동작에 대해서 정리하면서 객체들의 사용이 끝났음에도 메모리 해제가 제대로 이뤄지지 않는 경우가 있다고 했었는데 그 부분에 대해서 얘기해볼까 합니다. 또 몇가지 해결방법도 같이요!

순환참조가 뭐야? 왜 생기는거야?

순환참조가 어떤 것이고 어떤 상황에 발생하는 지 코드를 예시로 살펴보겠습니다.

class Traveler {
	var name: String
    var account: Account?
    func printSummary() {
    	if let account = account {
        	print("\(name) has \(account.points) points")
        }
    }
}

class Account {
	var traveler: Traveler
    var points: Int
}

func test() {
	let traveler = Traveler(name: "Lily")
	let account = Account(traveler: traveler, points: 1000)
	traveler.account = account
    traveler.printSummary()
}
          

위의 코드에서 Traveler는 Account 타입의 프로퍼티를 가질 수 있고 points를 계산할 수 있습니다.
또 Account도 Traveler 타입을 프로퍼티로 가집니다.

test() 함수에서는 Traveler와 Account 각각 객체를 만들고 traveler의 account 프로퍼티에 Account 객체를 할당해줍니다.

test() 함수를 한줄씩 ARC와 관련하여 어떻게 동작하는지 확인해보겠습니다.

먼저

let traveler = Traveler(name: "Lily")

에서 Traveler 객체가 힙에 생성됩니다. 생성되면서 reference count를 1 증가시킵니다.

다음

let account = Account(traveler: traveler, points: 1000)

에서 Account 객체가 힙에 생성되면서 Account 객체의 reference count를 1 증가시키고 Account 객체는 traveler를 프로퍼티로 가져 traveler의 reference count를 1 증가시켜 2가 됩니다.

그리고

traveler.account = account

에서 traveler의 accout 프로퍼티에 방금 생성한 account 객체를 할당해 traveler의 reference count를 2로 증가합니다.

여기 코드까지가 account 객체의 마지막 사용입니다.
그렇게 때문에 account 객체의 reference count를 1 감소시킵니다.

또 마지막 코드

traveler.printSummary()

에서 traveler 객체의 마지막 사용으로 reference count를 1 감소시킵니다.

그런데!! 어? 원래 객체의 lifetime이 끝나면 메모리 해제가 되어야하는데 account 객체와 traveler 객체의 reference count가 1이 남아있습니다!!

그 이유가 바로 순환참조! Reference Cycle 때문입니다.
위의 예시처럼 두 객체가 서로가 서로를 참조해서 생기는 현상입니다.

이렇게 되면 두 객체는 절대 메모리 해제되지 않아 메모리 leak이 발생합니다.

그럼 어떻게 해결해??

바로 말씀드리면 weakunowned 키워드가 있습니다.
이 둘은 참조하는 인스턴스의 reference count를 증가시키지 않게 하기 때문에 순환참조가 일어나지 않게 합니다.

weak와 unowned의 차이점은 객체가 deallocate(메모리 해제) 되었는데 객체에 접근하게 되면
weak reference는 nil을 반환하고
unowned reference는 trap 처리하는 것입니다.

그럼 위의 에시에서 weak 키워드를 한번 넣어보겠습니다.

class Account {
	// weak 키워드 추가 
	weak var traveler: Traveler?
    var points: Int
}
          

이렇게 되면 traveler를 약한 참조(weak reference)하기 때문에

let account = Account(traveler: traveler, points: 1000)

에서 account 객체가 생성될 때 traveler 객체의 reference count가 증가하지 않습니다.

그렇기 때문에

traveler.printSummary()

위의 코드에서 traveler 객체의 lifetime이 끝남으로써 reference count가 0이 되고 메모리 해제가 정상적으로 될 수 있는 것이죠.

또 traveler 객체가 정상적으로 메모리 해제됨으로써 traveler 객체의 account 객체의 참조가 사라지면서 account 객체의 reference count도 0이 되어 정상적으로 메모리 해제됩니다.

하지만! weak/unowned 를 사용할 때의 문제점도 있습니다.
만약 아래와 같이 되어있으면 어떻게 할까요?

class Traveler {
	var name: String
    var account: Account?
}

class Account {
	weak var traveler: Traveler?
    var points: Int
    func printSummary() {
    	print("\(traveler!.name) has \(points) points")
    }
}

func test() {
	let traveler = Traveler(name: "Lily")
	let account = Account(traveler: traveler, points: 1000)
	traveler.account = account
    account.printSummary()
}
          

아래 코드에서 traveler 객체는 메모리 해제됩니다.

traveler.account = account

그렇다면 아래 코드는 어떻게 실행될까요?

account.printSummary()

weak를 사용했기 때문에 account의 traveler 프로퍼티에 nil이 자동할당되어 crash가 발생합니다.

이런 부분도 잘 생각해서 코드를 작성해야할 것 같습니다.

WWDC 영상에서는 위의 문제점들도 해결할 수 있는 몇가지 방법을 설명해주는데 저는 여기까지만 정리해볼게요... ㅎㅎ
더 알고 싶으신 분은 아래 참고 링크를 타고 들어가셔서 보시면 될 거 같아요!

[참고]

profile
iOS 개발자

0개의 댓글