도니의 iOS 프로그래밍 세상

[Swift] Strong Reference Cycle - ARC 기초 2탄 본문

Swift

[Swift] Strong Reference Cycle - ARC 기초 2탄

Donee 2023. 1. 8. 16:05

안녕하세요, 지난 포스팅에선 ARC의 정의, ARC의 작동원리에 대해서 공부했습니다.
이번 포스팅에선 ARC의 Counting 방식으로 인해 생기는 Strong Referecne Cycle에 대해서 공부하곘습니다.

Memory Leak

지난 포스팅에선,

객체에 대한 count가 0이 됐다면, 더이상 메모리에서 사용하지 않는다고 판단하고 자동으로 ARC를 통해 클래스 "객체"를 메모리로부터 Free 시킨다.

라고 하였습니다. 즉 누구도 해당 instance에 접근하지 못한다고 판단하면 메모리에서 deallocate 시키는 것입니다.
하지만, 누구도 instance에 접근하지 못하고, 사용하지 않음에도 메모리에서 deallocate가 이뤄지지 않으면 어떻게 될까요?
Memory Leak이 발생할것입니다.
Swift에서 Memory Leak의 원인이 되는 가장 흔한 케이스가 바로 "Strong Reference Cycle"입니다.

Strong Reference

Strong Reference는 변수, 프로퍼티, 상수등으로 클래스 객체에 할당할때를 의미합니다.

var strongReference = SomeClass()

바로 이러한 상황입니다.
"Strong"이라는 이름이 붙은 이유는 해당 Reference가 남아있는 한, 메모리로부터 "해제"되지 않기 때문에 붙여졌습니다.

Strong Reference Cycle

만약, 두개의 클래스 객체가 서로를 "강한참조(Strong Reference)하고 있고, 서로를 참조를 떼어놓지 못한다면 어떻게 될까요?
이를 우리는 Strong reference라고 합니다.
말이 어려우니 코드로 확인하겠습니다

class Job {
    var name: Name?

    deinit {
        print("deinit Job")
    }
}

class Name {
    var job: Job?

    deinit {
        print("deinit Name")
    }

}


var a: Job? = Job()
var b: Name? = Name()


a?.name = b
b?.job = a

a는 Job객체를 갖고, b는 Name 객체를 갖습니다.
a의 Job객체 내부에서는 b를 갖고 있습니다. b또한 Name객체 내부에서 a가 갖고 있는 Job객체를 갖고 있죠

메모리 해제 여부

만약 이때, a와 b를 nil처리 해준다면 어떻게 될까요?

a = nil
b = nil

아무도 a가 가르키는 Job 객체와 b가 가르키는 Name객체에 접근할 수 없으니 deinit이 되어야 합니다.

과연, 메모리 해제가 되었음을 알리는 deinit이 정상적으로 호출될까요?

결과를 확인해봅시다!

결과

// Output
// 아무것도 호출되지 않습니다

Reference Count

아무것도 호출되지 않습니다. 왜이런 현상이 벌어질까요?

이전 포스팅에선 하나의 객체를 참조하는 변수들에 따라서 Reference Count가 올라간다고 배웠습니다.

그렇다면, a와 b를 nil 처리하기 전 a가 가르키는 Job 객체와 b가 가르키는 Name객체의 Reference Count는 몇일까요?

위의 그림과 같이 Reference Count는 2가 됩니다.(두개의 참조변수가 객체를 참조하고 있기 때문이죠.)

이때 nil을 처리하면 Reference Count는 몇이 될까요?

a, b 참조 변수가 객체를 참조하지 않았지만, 각자 클래스 객체 내부에서 서로를 호출하고 있기 때문에 Reference Count는 1이 됩니다.

Reference Count가 0이 되지 않는한 메모리에서 해제는 되지 않습니다.

따라서 서로는 서로를 참조하고 있는 구조에선, 한쪽이 해제가 되지 않는 한 계속해서 메모리에 남아 있게 됩니다.

그리하여 우린 서로를 참조하고 있는 이 구조를  "Cycle"을 가지고 있다고 표현합니다.

그리하여 Strong Reference Cycle이라고 표현하는 것이지요!

그럼 이러한 현상을 어떻게 해야할 수 있을까요?

Strong Reference Cycle 해결법

앞서 말한 Strong Reference Cycle은 Memory leak을 일으키는 주범으로서 반드시 해결해야하는 버그입니다
이를 해결하기 위해선 어떤 해결책을 적용할 수 있을까요?

  1. a와 b를 nil처리 하기 전 Job객체와 Name 객체가 서로 참조하고 있는 구조를 해제시켜 Reference count를 감소시킨다.
  2. Reference Count로 인한 문제기 때문에, Job객체와 Name객체가 서로 참조할 때 Reference Count를 증가시키지 않도록 한다.

1 - A. Job객체와 Name객체의 서로 참조 해제

var a: Job? = Job()
var b: Name? = Name()


a?.name = b
b?.job = a

a?.name = nil
b?.job = nil
a = nil
b = nil

위와 같이 처리하면 결과는 어떻게 될까요?

결과

// Output
deinit Job
deinit Name

서로 간 Reference를 해제시켰기 때문에 밑의 그림과 같이 Reference Count는 0이됩니다.
따라서, 정상적으로 메모리에서 사라집니다.

근데 반드시, 서로 해제를 시켜주어야만 메모리에서 해제될까요?
우리는 하나의 객체만 nil을 처리해도 상관이 없습니다. 예시를 볼게요.

1 - B. 두 객체간 서로 참조 구조중 하나만 해제

var a: Job? = Job()
var b: Name? = Name()


a?.name = b
b?.job = a

a?.name = nil
a = nil
b = nil

위와 같이 한쪽의 참조만 끊어주어도 해결될까요?
결과를 확인해볼게요!
결과

// Output
deinit Name
deinit Job

네, 정상적으로 작동합니다. 왜이럴까요?
그림을 통해서 확인하겠습니다.

null

첫번째 그림은 a의 name을 nil 시키고 나서입니다.

a?.name = nil

Name 객체의 Refernece Count는 1이 되었고, Job객체는 여전히 Reference Count가 2입니다.

그리고 나서 a와 b를 nil로 하면 어떻게 될까요?

null

그림과 같이, a는 Reference Count가 1이되고, b의 Reference Count가 0이 됩니다.
count가 0이기 때문에, b가 참조하는 Name의 객체가 deallocate됩니다. b가 deallocate되고 나선 어떻게 될까요?

null

그림은 b가 deallocate되고 나서 발생하는 상황입니다.

b가 deallocate됨에 따라서 Name이 참조하고 있는 Job객체에대한 참조또한 사라집니다
그러면 Job객체의 Reference Count는 0이 됩니다.

따라서 두개의 객체 모두 Reference Count가 0이 되기에 메모리에서 모두 해제됩니다.

1-A, 1-B 결론

  1. 객체 내 서로간의 참조 모두를 해제한다면, 정상적으로 메모리에서 해제된다.
  2. 객체 내에서 서로간 참조하는 구조중 하나의 참조만 해제도, 정상적으로 메모리에서 해제된다.
  3. 1 - B와 같이 Cycle이 발생한다면 하나만 끊어주어도 정상적으로 메모리에서 해제된다.

2. Reference Count를 증가시키지 않는 방법

Swift에서 Reference Count를 증가시키지 않는 방법은 무엇일까요?
바로 weak, unowned를 사용하는 것입니다. 이들은 Strong Reference와 달리 Reference Count를 증가시키지 않고 객체를 참조하는게 가능합니다.
이들이 무엇인지에 대해서는 ARC 3탄에서 확인하고 예시를 먼저 확인하겠습니다.

2 - A. weak 사용

1-A, 1-B 결론과 같이 Strong Reference Cycle은 Cycle중 하나의 참조만 끊어도 해결이 가능합니다.
그렇다면 Reference Count를 증가시키지 않는 weak를 한쪽 클래스에서만 사용해볼까요?

class Job {
    weak var name: Name?

    deinit {
        print("deinit Job")
    }
}

class Name {
    var job: Job?

    deinit {
        print("deinit Name")
    }

}


var a: Job? = Job()
var b: Name? = Name()

a?.name = b
b?.job = a

a = nil
b = nil

결과는 아래와 같습니다.

// Output
deinit Name
deinit Job

weak가 Name 객체의 Reference Count를 증가시키지 않습니다. 따라서 b가 가르키는 Name instance의 a가 가르키는 Job instance의 Count는 2입니다.

2 - B. unowned

unowned도 "weak"와 유사하게 reference count를 증가시키지 않습니다.
따라서 weak와 동일한 위치에 unowned를 넣겠습니다.

class Job {
    unowned var name: Name?

    deinit {
        print("deinit Job")
    }
}

class Name {
    var job: Job?

    deinit {
        print("deinit Name")
    }

}


var a: Job? = Job()
var b: Name? = Name()

a?.name = b
b?.job = a

a = nil
b = nil

결과는 2 - A 결과와 동일합니다.

// Output
deinit Name
deinit Job

2 - A, 2 - B 결론

  1. strong은 reference count를 증가시킨다. 그와 다르게 증가시키지 않는 방법이 "weak", "unowned"이다.
  2. weak, unowned는 reference count를 증가시키지 않는다.

 

 

결론

1. strong Reference는 프로퍼티, 변수등이 일반적으로 객체를 할당받을때 발생한다.

2. 이는 Reference Count를 증가시킨다.

3. Strong Referecne Cycle은 두 객체가 서로간 참조하는 구조일때 발생한다.

4. 이를 해결하기 위해서는 임의로 두 객체간의 참조중 "하나"를 해제한다.

5. 또다른 방법으로는 Reference Count를 증가시키지 않는 "weak", "unowned" 키워드를 사용한다.

 

다음 시간에는, ARC 3탄 unowned weak에 대해서 살펴보겠습니다! 

감사합니다.

Comments