본문 바로가기
개발 블로그/아이폰개발

ARC, 순환참조와 소유권 지시어 (iOS, xcode)

by snapshot 2019. 10. 22.

ARC, 순환참조와 소유권 지시어 (iOS, xcode)

"iOS와 OS 의 메모리 관리와 멀티스레딩 기법" 이란 오래된 책을 다시 요즘에 읽어 소유권 지시어에 대한 내용을 적어 본다.

ARC (Automatic Reference counting)은 메모리 관리를 컴파일러가 직접 한다. iOS 개발을 처음 시작 할 때는 retain/release의 코드를 직접 넣어 주어 메모리 관리를 했다. 하지만 ARC가 나오면서 release를 실수로 잘못 넣어주어 런타임 크래쉬 발생이 되는것을 고려 안 해도 되고 메모리 관리도 쉽게 도와주었다. 물론 순환 참조는 아직 유의를 해야 하지만..

일단 ARC가 나오면서 strong, weak라는 새로 등장한 소유권 지시어가 나왔기 때문에 설명을 이어가겠다.

 

 

레퍼런스 카운팅의 규칙

  • 직접 만든 객체는 소유권을 갖는다.
  • ratain을 사용해 객체의 소유권을 가져올 수 있다.( ARC 이전 이야기)
  • 객체가 더이상 필요하지 않으면 소유권을 제거해야 한다.
  • 소유하지 않은 객체의 소유권을 제거해서는 안 된다. (ARC 이전 이야기)

strong : 흔히 강한참조로 ARC 환경에서는 release 메서드가 컴파일시 자동으로 추가되고 참조시 reference count가 증가, 해제시 감소 되어 0이 되었을 경우 메모리에서 제거가 된다.

//밑의 예제는 책에서는 Objective - c로 되어 있지만 swift로 바꿔서 설명을 해봄

  •  

    class Object {
    
    }
    
    var obj0 : Object? = Object() // 객체 A
    // obj0은 객체 A에 강한 참조를 갖는다.
    
    var obj1 : Object? = Object() // 객체 B
    // obj1은 객체 B에 강한 참조를 갖는다.
    
    var obj2 : Object? = nil
    // obj2는 참조를 가지 않는다.
    
    obj0 = obj1
    
    /*
    
    obj0은 객체 B에 대한 강한 참조를 갖는데, obj1을 대입한다.
    따라서 obj0은 더이상 객체 A에 대한 강한 참조를 갖지 않는다.
    객체 A는 더이상 누구도 소유하지 않으므로 제거된다.
    이제, obj0과 obj1은 둘 모두 객체 B에 대한 강한 참조를 갖는다.
    */
    obj2 = obj0
    
    /*
    
    obj2는 obj0을 통해 객체 B에 대한 강한 참조를 갖는다.
    이 시점에서 obj0, obj1, obj2는 모두 객체 B에 대한 강한 참조를 갖는다.
    */
    obj1 = nil
    /*
    
    obj1에 nil이 대입되었으므로 객체 B에 대한 강한 참조는 사라진다.
    이 시점에서 obj0, obj2는 모두 객체 B에 대한 강한 참조를 갖는다.
    */
    obj0 = nil
    /*
    
    obj0에 nil이 대입되었으므로 객체 B에 대한 강한 참조는 사라진다.
    이 시점에서 obj2는 모두 객체 B에 대한 강한 참조를 갖는다.
    */
    obj2 = nil
    /*
    
    obj2에 nil이 대입되었으므로 객체 B에 대한 강한 참조는 사라진다.
    객체 B를 어느 누구도 소유하지 않게 되어 객체 B는 제거된다.
    */​

weak : strong 소유권으로 컴파일러가 메모리 관리를 충분히 수행 할 수 있어 보이지만 "순환 참조"라는 큰 문제는 피할 수가 없다. 그래서 나온게 weak 지시어이다.

순환 참조란?

순환 참조가 있으면 메모리 누수가 자주 발생한다. 메모리 누수는 특정 객체가 제거 될 시점 뒤에도 메모리에 여전히 존재하는 상황을 말함.

순환 참조는 A, B 서로 강한 참조를 갖고 있는 경우, 자기 참조 일 경우, 블록, 클로저에서 일어 날 수 있다.

이것은 약한참조로 strong과 달리 참조시 reference count가 증가 되지 않고 참조한 객체가 소멸되면 nil로 된다.

순환참조 예제1) 객체 A, B가 서로 참조할 경우

 

// 두 변수에 nil을 대입해서 해제를 하고 싶었지만 각 변수가 서로를 참조하고 있기에 둘다 메모리에 남음

class A {
var b : B?
}

class B {
var a : A?
}

let a = A() // a = 1
let b = B() // b = 1

a.b = b // b = 2
b.a = a // a = 2

a = nil // a = 1
b = nil // b = 1

// 두 변수에 nil을 대입해서 해제를 하고 싶었지만 각 변수가 서로를 참조하고 있기에 둘다 메모리에 남음

해결 방법

한쪽 변수에 weak 지시어를 사용

class A {
var theB : B?
}

class B {
var weak theA : A?
}

let a = A() // a = 1
let b = B() // b = 1

a.theB = b // b = 2
b.theA = a // a = 1

a = nil // a = 0 , b = 1
// a에 nil을 대입하면 a는 B의 변수 theA에 weak로 되어 있기에 theA에는 nil에 대입이 되고 a는 해제가 된다.
// 그와 동시에 A의 변수 theB도 해제가 된다.
b = nil // b = 0
// B 또한 nil을 대입하면 b의 reference count는 0이 되고 둘다 해제가 된다.

ARC를 자세히 들여다 보면

strong은 컴파일시에 relase 코드를 넣어주지만 weak는 weak만의 참조 테이블이 있다.

왜냐하면 참조한 변수가 해제 시 weak로 참조한 것에 모두 nil로 반납을 해줘야 하기 때문에 따로 관리가 된다.

weak의 메모리는 내부적으로 autorealase pool로 관리 된다고 한다. 그래서 약한 참조를 사용하는 것도 어찌보면 메모리를 사용하는 것이기 때문에 너무 남발을 하게 되면 메모리에 영향을 주어서 느려지는 원인이 된다.

그리고 순환 참조가 되는 경우가 또 있다.

그건 블록, 클로저의 경우 capturing 시 self를 참조할 경우 self의 reference count가 올라 가게 된다. __weak(objectvie c), weak self 캡쳐링 리스트를 사용하면 해결이 된다.

이건 블록, 클로저의 내용에서 다시 설명을 하겠다.

댓글