본문 바로가기
iOS📱/Swift

[ Swift ] ARC (2) Retain Cycle, 강한참조, 약한참조 그리고 미소유참조

by 텅빈비니 2022. 12. 29.
반응형

안녕하세요 🐶
빈 지식 채우기의 비니🙋🏻‍♂️ 입니다.

오늘은 ARC의 두 번째 시간입니다!

Retain Cycle과 참조 종류 3가지에 대해 알아보는 시간을 가지겠습니다.

 

1. 개요

저번 글에서는 ARC의 기본 개념과 Reference Count에 대해 알아보았습니다.오늘은 ARC가 제대로 작동할 수 없게 되는 이유에 대해 알아보고, 해결할 수 있는 방안에 대해 공부해보도록 하겠습니다. 

 

ARC의 개념에 대해 잘 모르시는 분은 아랫 글을 참고하면 감사드리겠습니다!

 

[ Swift ] ARC (1) 기본 개념, Reference Count 이해

안녕하세요 🐶 빈 지식 채우기의 비니🙋🏻‍♂️ 입니다. 오늘은 ARC의 기본 개념과 Reference Count에 대해 알아보는 시간을 가지겠습니다. 1. 개요 ARC ( Auto Reference Counting ) : 말 그대로 '자동 참조

beanistory.tistory.com

 

2. 메모리 누수 - Retain Cycle ( 강한 순환 참조 )

ARC는 자동으로 메모리 관리를 해주고 참조 카운트를 통해 적절하게 메모리를 해제시켜준다.

메모리 참조는 크게 강한참조, 약한참조, 미소유참조 가 있다.우리가 기본적으로 상수, 변수 등 프로퍼티를 선언할 때 별도의 식별자를 작성하지 않으면 기본으로 강한참조를 하게 된다. 

흔히 사용하는 var, let 이런 것이 다 강한참조 였었다..두둥!

 

그런데 여러개의 참조를 발생하는 상황에서, 강한 참조를 잘못 사용하면 메모리 누수 문제가 발생할 수 있다.

이 문제를 바로 강한 순환 참조 Retain Cycle 이라고 한다.

대표적으로 인스턴스 내 프로퍼티가 서로를 강한 참조할 때가 대표적으로 일어난다!

 

아래에 Retain Cycle이 일어나는 대표적인 사례를 작성해 보았다.

class RC_Test1 {
    var A: RC_Test2?    // (1)
    
    init() {
        print("RC_Test1 Init")
    }
    
    deinit() {
        print("RC_Test1 deInit")
    }
}

class RC_Test2 {
    var A: RC_Test1?    // (1)
    
    init() {
        print("RC_Test2 Init")
    }
    
    deinit() {
        print("RC_Test2 deInit")
    }
}

var RC1 = RC_Test1()    // (2)
var RC2 = RC_Test2()

RC1.A = RC2    // (3)
RC2.B = RC1

RC1 = nil    // (4)
RC2 = nil
  1. RC_Test1, RC_Test2 클래스에는 각 클래스를 참조하는 프로퍼티가 존재한다.
  2. 이때 각각의 클래스에 대해 인스턴스를 생성한다.
    • 현재 각 인스턴스의 참조 카운터
      • RC_Test1 : 1
      • RC_Test2 : 1
  3. 추가로, 두 인스턴스의 각각 프로퍼티에 서로의 클래스를 참조하도록 하였다.
    • 현재 각 인스턴스의 참조 카운터
      • RC_Test1 : 2
      • RC_Test2 : 2
  4. 그 후, RC1과 RC2를 동시에 nil로 할당한다
    • 현재 각 인스턴스의 참조 카운터
      • RC_Test1 : 1
      • RC_Test2 : 1.  

 

4번 결과 이후에 문제가 발생하게 된다.

1) RC1과 RC2를 동시에 nil로 할당하였다.
2) 각각의 인스턴스를 접근할 수 있는 경로가 사라졌다.
3) nil 할당으로 참조 카운터가 1이 감소하지만, 더 이상 감소시킬 수 있는 방법이 없다.

따라서 RC1과 RC2 인스턴스 모두 참조 카운터가 1로 남게되고

새로운 참조 카운터를 감소 시킬 수 있는 방법이 없기 때문에 메모리에 계속 남게 된다. 

이것이 바로 메모리 누수이다.

 

이 문제를 해결하기 위해 우리는 아래와 같은 방법을 사용할 수는 있다.

RC1.A = nil
RC1 = nil

RC2.B = nil
RC2 = nil

다만 이러한 방법은 개발자 실수가 일어날 수 있고, 복잡한 클래스의 경우는 해결하기 어려울 것이다.

이때 나온 방법이 약한 참조와 미소유 참조이다.

 

3. Retain Cycle 해결 (1) - Weak Reference ( 약한 참조 )

약한 참조는 참조하는 인스턴스의 참조 카운트를 증가시키지 않는다.

약한 참조가 어떻게 쓰고, 어떻게 동작하는지 확인해보자.

class RC_Test1 {
    var A: RC_Test2?    // (1)
    
    init() {
        print("RC_Test1 Init")
    }
    
    deinit() {
        print("RC_Test1 deInit")
    }
}

class RC_Test2 {
    weak var A: RC_Test1?    // (1)
    
    init() {
        print("RC_Test2 Init")
    }
    
    deinit() {
        print("RC_Test2 deInit")
    }
}

var RC1 = RC_Test1()    // (2)
var RC2 = RC_Test2()

RC1.A = RC2    // (3)
RC2.B = RC1

RC1 = nil    // (4)
RC2 = nil
  1. RC_Test1, RC_Test2 클래스에는 각 클래스를 참조하는 프로퍼티가 존재한다.
  2. 이때 각각의 클래스에 대해 인스턴스를 생성한다.
    • 현재 각 인스턴스의 참조 카운터
      • RC_Test1 : 1
      • RC_Test2 : 1
  3. 추가로, 두 인스턴스의 각각 프로퍼티에 서로의 클래스를 참조하도록 하였다.
    • 현재 각 인스턴스의 참조 카운터
      • RC_Test1 : 1 ( weak 으로 인해 참조 카운트 증가 X )
      • RC_Test2 : 2
  4. 그 후, RC1과 RC2를 동시에 nil로 할당한다
    • 현재 각 인스턴스의 참조 카운터
      • RC_Test1 : 0 ( "RC_Test1 deInit" 출력 )
      • RC_Test2 : 0 ( "RC_Test1 deInit" 출력 )

 

Q1) RC_Test2를 참조하는 RC1에만 nil을 할당했는데, 왜 RC_Test2까지 참조 횟수가 감소했을까?
그 이유는 매우 간단하다.
> RC1 인스턴스가 해제되면서 자연스럽게 RC1의 프로퍼티인 A 또한 역할이 끝나게 된다.
> A는 약한 참조로 되어있기 때문에 RC2의 참조 카운트까지 감소되는 것이다.
Q2) print( RC2.B ) 를 했을 시 왜 nil 을 반환하는가? 
> RC2 인스턴스의 B는 약한 참조를  하므로 RC1의 인스턴스가 메모리 해제가 되었으니 자동으로 nil 을 할당한다.

 

3-1. 정리

  • Q1
    1. 어떤 인스턴스가 메모리에서 해제가 된다.
    2. 그와 동시에 해제된 인스턴스의 프로퍼티가 약한 참조하는 인스턴스의 참조 카운트도 1 감소된다.
  • Q2
    1. 약한 참조를 하는 프로퍼티가 참조하는 인스턴스가 해제가 된다.
    2. 자동으로 nil을 할당하기 때문에, 항상 꼭! 옵셔널 타입으로 선언해야 한다.

 

4. Retain Cycle 해결 (2) - Unowned Reference ( 미소유 참조 )

미소유 참조 또한 약한 참조처럼 참조 카운트를 증가시키지 않는 참조 방법이다.

이 둘의 차이 점은 아래와 같다.

  • 미소유 참조의 경우 내가 참조하는 인스턴스가 꼭 메모리에 존재할 것이라는 전제가 있어야 한다.

이게 대체 무슨소리야..-_-  ㅎㅎ.. 빠르게 코드로 알아보자!

 

class Person {
    let name: String
    var car: Car?    // (1)
    
    init(name: String) {
        self.name = name
        print("\(name) being initialized")
    }
    
    deinit {
        print("\(name) being deinitialized")
    }
}

class Car {
    let name: String
    unowned var owner: Person    // (2)
    
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
        print("\(name) being initialized")
    }
    
    deinit {
        print("\(name) being deinitialized")
    }
}

var seolhee: Person? = Person(name: "seolhee")    // (3)

if let person: Person = seolhee {
    person.car = Car(name: "tivoli", owner: person)    // (4)
}

seolhee = nil    // (5)
  1. Person은 Car를 꼭 가지지 않아도 된다.
    • 강한 참조, 옵셔널로 선언
  2. Car은 Person(주인)이 꼭 있어야 한다.
    • 미소유 참조로 선언
  3. Person 인스턴스 생성
    • 현재 각 인스턴스의 참조 카운터
      • seolhee : 1
  4. Car 인스턴스 생성
    • 현재 각 인스턴스의 참조 카운터
      • Person ( seolhee ) : 1
      • Car : 1
  5. Person ( seolhee ) 인스턴스 nil 할당
    • 현재 각 인스턴스의 참조 카운터
      • Person ( seolhee ) : 0
      • Car : 0

 

Q) 그러니까 약한 참조와 미소유 참조의 차이가 정확히 무엇인가요?
> 가장 핵심적인 차이를 모여주는 구문은 (4) 번이다.
> if let 을 통해 Person 인스턴스가 있어야만 Car 인스턴스를 생성 가능하게 구현되어 있다.
( 앞서 말한대로 참조하는 인스턴스는 항상 메모리에 올라와 있는 것을 기준으로 하기 때문!! )
> 그 뒤로는 약한 참조와 마찬가지로 Person 인스턴스를 nil로 할당하였고, Car 인스턴스도 모두 해제가 되었다.

이렇게 미소유 참조를 하게 되면 참조하는 인스턴스의 제약을 쉽게 줄 수 있고

강한 순환 참조 문제도 피해갈 수 있다.

 

5. 정리

  • Retain Cycle 
    • 각각이 인스턴스가 서로를 강한 참조를 하게 될 경우 발생하는 문제!
    • 그로 인해 메모리 누수가 발생하여 성능 저하를 일으키게 된다.
  • 약한 참조 
    • 참조 카운트를 증가시키지 않는다.
    • 내가 참조하는 인스턴스가 메모리에서 해제가 되었을 경우 자동으로 nil을 할당해준다.
    • 반드시 옵셔널 변수로 선언!
  • 미소유 참조
    • 참조 카운트를 증가시키지 않는다.
    • 내가 참조하는 인스턴스는 반드시 메모리에 존재한다는 가정하에 사용하는 참조
    • 옵셔널 변수로 선언하지 않아도 된다!

 

이상으로 ARC (2) Retain Cycle, 강한참조, 약한참조 그리고 미소유참조 포스팅을 마치겠습니다.
틀린 부분이나 궁금한 사항은 댓글 남겨주세요~

 


참고

반응형