2015년 1월 5일 월요일

Swift Memory Management #2 ARC 기초

앞서 Objective-C의 레퍼런스 카운트 개념에 대한 기본적인 지식을 설명했다. 이제 Swift 영역으로 돌아와서, ARC에 대해 좀 더 깊이 들어가 보자.

이제는 Xcode에서 Swift 프로젝트를 생성하면 ARC가 기본적으로 사용되도록 설정된다. (사실 Swift 프로젝트에서 ARC를 끌 수 있는지 조차 잘 모르겠다)

Objective-C와 비슷하게, Swift도 ARC라는 개념은 동일하게 사용된다. 자동으로 빌드 단계에서 retain - release 코드를 삽입하는 방식 말이다.
// 예제 코드
func someFunction() {
    var obj = SomeClass()
    doWork(obj)
}

// 위 코드는 빌드 시 ARC에 의해 아래와 같은 식으로 바뀌어져 컴파일된다.
// 다만 아래 코드는 개념 설명을 위한 코드이므로 빌드는 안 될 것이다.
func someFunction() {
    var obj = SomeClass()
    obj.retain()       // ARC

    doWork(obj)

    obj.release()      // ARC
}
Swift를 처음 접한다면 깊게 생각할 필요는 없다. 애초에 Swift는 "Just works" 라는 것을 중요하게 삼는 언어이다. 그래서 ARC를 모른 채로 언어 공부를 시작했다고 해도 무리는 없는 문법을 갖추고 있다.

그런데 그냥 무작정 ARC에 맡기기에는 프로그래머의 의도가 컴파일러에게 전달되지 않아서 문제가 되는 경우가 생길 것이다. 특히 메모리를 소유하는 주체가 누구냐인 점은 컴파일러가 판단 할 수가 없다. 예를 들어 B 클래스에서 할당된 메모리를 A 클래스에서 사용해야 된다면 말이다.
주의사항: 다수의 Swift로 작성된 예제코드가 나오는데 마치 플레이그라운드(Playground)용 코드로 보인다. 하지만 플레이그라운드에서 실행시킬 경우 deinit이 호출되는 모습을 볼 수가 없어서 제대로 공부하기엔 힘들 것이다. 플레이그라운드의 모니터링 특성 상 종료하기 전 까지 상당수의 메모리에 추가 리테인을 걸어두기 때문이다. 실제로 실습해 보려는 열성 넘치는(?) 독자 분께서는 플레이그라운드가 아닌 일반 앱 코드에 적절히 편집해서 실습 해보자.

세 가지 방법

Objective-C의 ARC와 비슷하게, Swift는 세 가지 방벙 중 하나를 골라 ARC에게 처리 방법을 알려 줄 수 있다.
  1. strong: 값 지정 시점에 retain이 되고 참조가 종료되는 시점에 release가 된다. 기본이기 때문에 별 다른 키워드가 없을 경우 자동으로 strong이 된다. 즉 특정 클래스가 소유하고 관리하는 프로퍼티는 strong이 적당하다.
  2. weak: 자신이 참조는 하지만 weak 메모리를 해제시킬 수 있는 권한은 다른 클래스에 있다. 그래서 값 지정 시 리테인이 발생하지 않는다. 따라서 릴리즈도 발생하지 않는다. 그래서 언제 어떻게 메모리가 해제될 지 알 수가 없다. 다만 메모리가 해제될 경우 자동으로 레퍼런스가 nil로 초기화를 해 준다. nil이 될 수 있기 때문에 반드시 Optionals 타입이 되어야 한다.
  3. unowned: weak와 비슷하지만 메모리가 해제되어도 레퍼런스가 초기화 되지는 않는다. 즉 non-optionals 타입으로 사용이 가능하다. 자신의 소유가 아니긴 하지만 참조 도중에 메모리가 해제되지 않는 확실한 로직에 이용하기 좋다.​
이 키워드는 아래와 같이 그냥 변수 선언 제일 앞에 붙여주면 된다. (물론 엑세스 컨트롤 코드 다음에 와야 하겠지만... 대충 앞이다)
class SomeClass {
    var a: AnotherClass?          // strong
    weak var b: AnotherClass?     // weak
    unowned var c: AnotherClass? // unowned
}
세 가지 모두 Optionals로 정의를 했는데, weak일 때만 optionals가 강제된다는 점을 기억하자.

strong​이 기본이라는 점을 잘 생각해 보자. 가장 많이 사용된다는 이야기이다. 사실 대부분의 단순한 코드의 경우 ARC 라는 것을 아예 생각하지 않고 코딩해도 될 정도이다.

Retain Cycles

특정 클래스에 아무런 명시 없이 코딩한 프로퍼티는 자동으로 strong 프로퍼티가 된다. 이 프로퍼티는 ARC에 의해 해당 클래스 오브젝트의 레퍼런스 카운트가 0이 되어 메모리에서 해제되어야 하는 시점에 각 프로퍼티들도 릴리즈된다. 따라서 가장 일반적이며 편하게 사용 할 수 있다.

그런데 두 오브젝트가 자신의 strong한 프로퍼티를 이용해 서로를 강하게 붙잡고 있는 경우 리테인을 건 상대방 프로퍼티 때문에 메모리 해제까지 가지 못 하는 상황이 발생 할 수 있다. 이 경우를 Retain Cycles 이라 부른다.
마치 두 사람이 서로의 물건을 독차지 하기 위해 ‘내껀 내꺼 네꺼도 내꺼’ 라고 말하며 격렬히 싸우고 있는 것과 비슷하다.
단순한 예를 하나 보자.
class SomeClass {
    var friend: SomeClass? = nil
}

var a = SomeClass()
var b = SomeClass()

a.friend = b
b.friend = a
위 경우 a와 b라는 오브젝트는 프로퍼티로 상대방의 레퍼런스(포인터)를 strong하게 가지게 된다. ARC가 자동으로 릴리즈 코드를 넣어줬다고 해도 a와 b는 절대로 메모리에서 해제되지 못 한다. ARC가 넣어준 릴리즈 코드가 동작하기 위한 조건이 부족하다고 생각하자.
ARC가 혼란에 걸렸다!
다만 이 경우는 굉장히 단순하기 때문에 friend 프로퍼티를 nil로 초기화를 해 주면 메모리가 해제된다. 특히 순환(Cycles)이기 때문에 둘 중 하나만 연결고리를 끊도록 해 주면 된다. 위 예제를 제대로 풀어내려면 아래 코드 중 한 라인만 추가하면 된다.
a.friend = nil
// or
b.friend = nil
이렇게 하면 한 쪽이 끊어지기 때문에 다른 한 쪽은 자연스럽게 해제가 가능해진다.
왜 이렇게 하면 메모리가 해제되는지 이해가 안된다면 다시 이 앞의 글에서 다룬 'ARC에서 값을 지정할 때 retain - release가 어떻게 추가되는지'를 생각해보자. a.friend = nil 이라는 구분은 a.friend가 가지고 있던 메모리에 릴리즈를 날려주고 나서 nil을 대입한다.

이제 약간은 더 복잡한 예제를 보자. 위와 동일한 클래스 구조를 이용한다.
class SomeClass {
    var friend: SomeClass? = nil
}

var a: SomeClass? = SomeClass()
var b = SomeClass()
var c = SomeClass()

a.friend = b
b.friend = c
c.friend = b

a = nil
위 예제는 참고용 예제이다. 왜냐하면 위 코드는 문제를 해결하기가 너무 쉽기 때문이다. 아래에 이 예제와 동일하지만 다른 방식으로 코딩된 작성된 예제를 보자.
class SomeClass {
    var friend: SomeClass? = nil
}

var a: SomeClass? = SomeClass()        // A
a!.friend = SomeClass()                // A.friend = B
a!.friend!.friend = SomeClass()        // B.friend = C
a!.friend!.friend!.friend = a!.friend  // C.friend = B

a = nil    // BOOM!
사실 가장 회피해야 할 코드 방식이지만 그래도 리테인 사이클을 설명하기에 중요한 코드가 바로 이런 식이다. 실질적인 동작은 앞서 언급한 참고용 예제와 동일하다.

이 코드에서 a가 nil이 되면 a가 가리키던 메모리는 레퍼런스 카운트가 0이 되어서 해제가 된다. 그런데 B와 C라는 이름이 붙은 녀석들은 앞서 본 단순한 사이클 처럼 영원히 해제되지 않는다.

그런데 마지막 a를 nil로 어사인해서 a에서 연결되는 메모리들의 엔트리 포인트(시작점)을 잃어버림으로써 큰 문제가 발생한다. B와 C라고 칭해둔 부분을 변수로 남기지 않았기 때문에 이 둘의 프로퍼티를 nil로 초기화 하는 방법 조차 사라졌다. a가 nil이 되지 않았다면 friend 프로퍼티를 추적해서 처리가 가능하겠지만 이미 nil이 되어버렸다. 결론적으로 B와 C로 표시한 메모리들은 영원히 해제되지 않고 해제할 방법도 없다.

물론 아래 처럼 미리 사이클을 끊어주는 식으로 코딩하는게 숙련된 프로그래머의 능력일 것이다.
a!.friend!.friend = nil
a!.friend = nil
이런 식의 괴랄한 메모리 해제 코드를 쓰고 싶진 않을 것이다. 물론 애초에 이런 식으로 만들지 않는게 현명하겠지만...

제대로 이해가 되는지는 잘 모르겠지만, strong은 좋지만 잘못 쓰면 위험한 경우가 있다는 이야기다. -_-;;

Retain Cycles의 회피 - weak

Retain Cycles 문제에 대해 길게 살펴본 이유가 있다면 바로 weak를 소개해야 하기 때문이다.

위 코드에서 friend를 weak로 명시하면 strong으로 인해 발생하는 사이클 문제가 해결 될 거라고 생각된다.
class SomeClass {
    // 이제 friend 프로퍼티는 약해짐(weak)
    weak var friend: SomeClass? = nil
}

var a: SomeClass? = SomeClass()       
a.friend = SomeClass()                
a.friend!.friend = SomeClass()        // Boom! (어?!)
a.friend!.friend!.friend = a!.friend

a = nil
이제 사이클 문제가 해결될... 아니 해결되어야 할 것 같지만 아마 제대로 실행되지 못 하고 죽을 것이다. 앞서 이야기 했다싶이 weak는 리테인을 걸지 않는다. 따라서 a!.friend = SomeClass() 라는 구문에서 생성된 SomeClass 인스턴스는 그 자리에서 바로 사라져버린다. 그리고 a.friend는 nil 된다. 그래서 Boom! 이라고 주석에 표시한 라인을 실행시키다 죽게된다. weak는 사용시 이런 점이 있으니 잘 파악하고 사용해야 한다.

위 문제를 풀기 위해서는 결국 사라져 버리는 인스턴스를 어디선가 리테인 걸도록 만들어 줘야 할 것이다.
class SomeClass {
    weak var friend: SomeClass? = nil
}

var a: SomeClass? = SomeClass()
var b = SomeClass() // 1
var c = SomeClass() // 2

a!.friend = b
b.friend = c
c.friend = b

a = nil
마지막 a가 nil이 되면 a가 메모리에서 해제된다. 물론 서로를 참조하던 b와 c도 자동으로 해제가 된다.

잘 이해가 안된다면 ARC의 개념부터 다시 생각해보자. 위 코드의 주석에서 1과 2로 표시한 코드에서 retain이 발생한다. 그리고 이 코드가 끝나는 부분에 ARC에 의해 b와 c를 릴리즈 하는 코드가 추가된다. 이외의 코드에서는 weak 프로퍼티만이 참조를 하기에 b와 c는 더 이상 리테인이 발생하는 곳이 없다. 따라서 위 코드가 끝나는 시점에서 b와 c도 레퍼런스 카운트가 0이 되어서 메모리가 해제되는 것이다.

물론 위 코드는 리테인 사이클 문제 회피를 설명하려다 보니 이렇게 되었지만 실제로 b와 c는 다른 곳에서 생성된 오브젝트일 가능성이 많을 것이다.

이제 weak의 용도에 대해 약간은 감을 잡았을지도 모른다는 희망을 가져도 될까? ;;;;

잡설

가독성이 떨어지는 예제코드 투성이라 좀 미안하다. 예제코드를 작성할 만한 내 능력이 여기까지 인가 보다.

unowned에 대해서는 빼먹었는데 non-optionals, 즉 nil로 초기화가 되지 않는다는 점만 빼면 weak와 거의 동일하므로 그냥 넘어가자. 추후에 closure에 관한 추가 포스트에서 거론 될 가능성도 있다.

[이전글] Swift Memory Management #1 기초 개념
[다음글] Swift Memory Management #3 구조체(struct)와 클래스(class)

댓글 1개 :

nuroo :

덕분에 많이 알아갑니다. 감사합니다.