Retain Cycle

Wonbi·2022년 9월 19일

✅ 학습 내용

💎 Retain Cycle

  • Delegate를 공부하던 도중, AnyObjectweak에 대해 알게 되었다. 이 두가지는 결국 Retain Cycle을 피하기 위해 채택하고 작성하는 키워드들인데, Retain Cycle에 대해 조금 더 톺아보기로 하자.

✏️ ARC

  • Retain Cycle을 이해하기 위해, 먼저 ARC에 대해 알아야 한다. ARC(Automatic Reference Counting)는 스위프트가 대부분의 메모리 관리를 대신 해주는 기능이다.

  • 이건 매우 좋은 소식이다. 우리는 덕분에 클래스와 같이 참조하는 값들을 선언할 때 따로 참조 카운트를 신경쓰지 않고 코딩을 할 수 있다.(일단은.. 우리가 지금까지 하던 프로젝트에 한해서는 그랬다.)

  • ARC의 원리 자체는 간단하다. 기본적으로 클래스의 객체를 포인팅하는 각각의 참조는 강한참조 이다. 최소한 하나의 강한참조가 있다면, 이 객체의 메모리는 해제되지 않는다. 그리고 강한참조가 하나도 없다면, 이 객체는 메모리에서 자동으로 해제될 것이다.

✏️ 그럼 알아서 다 해주니까 필요 없겠네?

  • 당연히~ 아니니까 이 공부를 하는 것이다. 사실, ARC는 정말 일을 잘 하고 있고, 지금까지는 괜찮았다. 하지만 조금씩 예외가 생기고 있다.(벌써 Delegate만 봐도 그렇다.)

  • ARC가 만능은 아니기 때문에, 우리는 특정 상황에 약간의 도움을 줘야 한다.

class Wonbi {
    var wonbi: Wonbi?
    
    init() {
        print("나.. 메모리에 등.장.")
    }
    
    deinit {
        print("나.. 메모리에서 퇴.장.")
    }
}

var person1: Wonbi? = Wonbi()    // "나.. 메모리에 등.장."이 프린트 됨.
var person2: Wonbi? = Wonbi()    // "나.. 메모리에 등.장."이 프린트 됨.

person1?.wonbi = person2
person2?.wonbi = person1
  • 여기 Wonbi라는 중2병 걸린 것 같은 타입이 있다. 이 타입은 wonbi라는 자기자신 타입의 프로퍼티와, 초기화 될 때랑 메모리에서 해제될 때 이상한 말을 내뱉는 이상한 타입이다. (실제로 나는 이런 사람 아니다.)

  • 그리고, 놀랍게도 person1person2Wonbi타입의 인스턴스이다. 초기화까지 잘 되었다. 초기화 되자마자 바로 중2병 스러운 멘트까지 치고 있다. 정말 놀라워!

  • 그다음, 각 인스턴스들(person1person2)이 wonbi라는 프로퍼티를 이용해 서로를 참조 하도록 해주었다.

  • 위 상황을 그림으로 한번 표현해보았다. 아 참고로 이거 그리는 사이트는 여기다. 캠퍼 미니가 알려줬다. 압도적 감사!

  • 그리고보니 뭔가.. 뭔가 일어나고 있다. 클래스는 참조타입이기에 인스턴스화를 하면 값을 참조 한다. 그림처럼! 자 다음에는 이렇게 해보자.

person1 = nil
person2 = nil
  • person1person2는 이제 nil을 할당 받음으로써 참조가 사라졌다. 그럼 메모리에서 해제되어야 한다. 바로 중2병 스러운 멘트를 또 뱉어낼 것이다.

  • 근데 실행해보면 안 뱉는다. 못믿겠다고? 직접 보여주겠다.

  • 어 진짜네?

  • 현재 상황이 이런 상태이기 때문이다. 맨 처음 ARC를 보면, "최소한 하나의 강한참조가 있다면, 이 객체의 메모리는 해제되지 않는다." 라고 말하고있다. 지금 보면 딱! 강한참조가 서로에게 하나씩 있다. 그래서 이 인스턴스들은 메모리에서 해제되지 않는다.

  • 게다가, 우리는 더이상 코드를 통해 이 인스턴스에 접근할 수 없다. person1person2nil을 할당하면서 각 인스턴스의 참조를 없애버리고 말았다.

  • 이 상황이 우리가 만든 앱 어딘가에 들어있다면.. 그리고 그게 여러개라면..?

for _ in 0...10000 {
    var person1: Wonbi? = Wonbi()
    var person2: Wonbi? = Wonbi()

    person1?.wonbi = person2
    person2?.wonbi = person1

    person1 = nil
    person2 = nil
}
  • 결국 앱이 강제종료가 되버리는 심각한 버그가 생길 것이다. 이러한 메모리 누수 (memory leak) 를 발생시키는 순환을 Retain Cycle이라한다.

✏️ 그럼 어떻게 해야하나?

  • 다 방법이 있다. 여기서 등장하는 것이 바로 약한참조 (weak reference) 이다. 이 약한참조의 사용은 Retain Cycle을 피하는 방법 중 하나이다.

  • 우리가 참조를 weak참조로 선언한다면, 이는 강한참조가 아닌 약한참조가 된다. 약한참조가 되면, 스위프트는 이 참조를 강한참조로 보지 않기 때문에 이를 제외하고 참조를 계산하게 된다.

class Wonbi {
    weak var wonbi: Wonbi? // 이제 이 프로퍼티는 약한 참조가 된다.
    
    init() {
        print("나.. 메모리에 등.장.")
    }
    
    deinit {
        print("나.. 메모리에서 퇴.장.")
    }
}

var person1: Wonbi? = Wonbi()
var person2: Wonbi? = Wonbi()

person1?.wonbi = person2
person2?.wonbi = person1
  • 약한 참조가 생겼다. 그림으로 표현하면..

  • 다시한번 미니에게 감사를...

  • 그럼 이제 nil을 할당해보자.

person1 = nil
person2 = nil
  • 그 결과는?

  • 그림으로 표현하면

  • 중2병 멘트를 뱉어내며 메모리에서 산화되는 두 인간을 볼 수 있다.

  • 이로써 우리는 weak키워드를 통해 약한참조를 지정해줌으로써 Retain Cycle을 피할 수 있게 되었다.

✏️ 주의점!

  • 매우 중요한 부분이 하나 있는데, 인스턴스를 메모리에서 해제하고 나면, 해당 변수에 대한 응답이 nil이 된다.(위 예제에서 인스턴스 안에 있는 프로퍼티가 nil이 된다는 의미)

  • 만약 nil이 아니라면 이미 메모리상에서 해제된 객체의 영역을 변수가 참조하는 것이 되버리므로 런타임 에러가 발생하게 된다.

  • 따라서 약한참조의 변수는 nil값을 언젠가 할당 받을 가능성이 분명하기 때문에 반드시 옵셔널 타입이어야만 한다.


Automatic Reference Counting
Retain Cycles, Weak and Unowned in Swift
[Swift] weak, unowned, Retain cycle 톺아보기

0개의 댓글