본문 바로가기

SWIFT

스위프트 ARC와 순환참조와 클로저

스위프트 ARC와 순환참조와 클로저

스위프트의 클로저의 기능중에 많은 사람들이 잘못 이해하고 있거나 잘 모르는 것이 클로저의 캡쳐리스트 (closure capture list)입니다. 주변 환경의 범위에서 가져온(참조한) 변수들을 얼마나 강하게 캡쳐해야하는지를 명시하는 것으로 캡쳐리스트를 사용하여 메모리 누수를 일으키는 강한 순환 참조(strong reference cycle)를 피할 수 있게됩니다.

먼저 강한 순환 참조에 대해서 알아 봅시다.

자동 레퍼런스 카운팅 (ARC: Automatic Reference Counting)

iOS는 레퍼런스 카운팅을 통해 레퍼런스가 더 이상 사용되지 않는 시점을 결정하여 레퍼런스가 할당받아 사용하던 메모리를 해제할 수 있도록 만듭니다. 개념적으로 간단한 것으로 프로퍼티, 상수, 변수에 레퍼런스가 지정되면 때 여기에 들어있는 카운트를 증가시키고 프로퍼티, 상수, 변수가 해제되면 카운트를 감소시킵니다. 보유한 카운트가 0이 되면 메모리를 해제시킵니다.

강한 순환 참조 (Strong Reference Cycles)

ARC의 한 가지 단점은 두 개의 객체가 상호 참조하는 경우와 같은 강한 순환 참조가 만들어 질 수 있다는 점입니다. 이렇게 되면 이 순환 참조에 연관된 객체들은 레퍼런스 카운트가 0에 도달하지 않게 되고 결국 메모리 누수가 발생하게 됩니다.

스위프트와 같은 ARC를 기반으로 하는 메모리 관리 모델에서는 어떻게 하면 강한 순환 참조를 발생시키지 않도록 하느냐가 메모리 관리상의 가장 큰 관심사가 됩니다.

순환 참조 상황을 이해하기 위한 예제로 Parent라는 객체가 있다고 가정해 봅시다. Parent 객체는 Child라는 객체를 생성합니다. Child객체는 자신의 부모가 누구이지 프로퍼티 myParent Parent객체에 대한 레퍼런스로 가지고 있으며 Parent는 자신이 생성한 자식에 대한 프로퍼티로 myChild를 가지고 있습니다. 이렇게 parent가 child에 대한 레퍼런스를 유지하고 child는 parent를 레퍼런스를 유지하는 상태를 순환 참조라고 합니다. 여기서는 간단히 두 클래스 간의 순환 참조를 예를 들었지만 훨씬 많은 수의 클래스, 구조체, 열거형 들이 순환 참조 고리를 구성하는 경우도 발생할 수 있습니다.

처음에 Parent 객체를 생성하면 레퍼런스 카운트는 1이 됩니다. 이후에 createChild()를 호출하게 되면 Child 객체가 생기면서 Child가 myParent로 Parent객체를 참조하게 되면서 카운트는 2로 증가합니다. 동시에 Parent의 myChild 프로퍼티가 Child 객체를 참조하므로 Child 객체의 레퍼런스는 1이 됩니다. 아래의 예제 코드의 마지막 행이 실행된 시점에서 부모 객체의 레퍼런스 카운트는 2, 자식 객체의 레퍼런스 카운트는 1입니다.

class Parent {
    var myChild:Child?

    func createChild() -> Child {
        let child = Child(parent:self)
        self.myChild = child
        return child
    }
}

class Child {
    var myParent:Parent

    init(parent:Parent) {
        self.myParent = parent
    }
}

var parent = Parent()
parent.createChild()

이제 위의 예제 코드 블럭이 종료되고 parent 변수가 더 이상 사용되지 않아서 Parent객체에 대한 레퍼런스 카운트가 1로 감소하게 된 경우를 생각해 봅시다. 부모 객체는 자식 객체의 프로퍼티로 인하여 1, 마찬가지로 자식 객체는 부모 객체의 프로퍼티로 인하여 1을 유지하게 됩니다. 두 객체 모두 레퍼런스 카운트가 0으로 감소할 수 없게 됩니다. 따라서 컴파일러가 자동으로 만들어 둔 두 객체에 할당된 메모리를 OS로 반납하는 코드는 영원히 호출될 수 없게 되어 메모리 누수가 발생 합니다.

fig

순환 참조 방지

객체에 대한 레퍼런스를 정의할 때 추가적인 속성을 지정할 수 있습니다. strongweakunowned 세 가지 중에 하나를 선택할 수 있습니다. strong가 디폴트 입니다. 즉, 이제까지 보여준 모든 예제에서 처럼 별도의 지시자를 기술하지 않았을 때 strong을 명시한 것과 동일한 효과입니다. 그 의미는 해당 레퍼런스에 대해 강한 참조(strong reference)를 유지 하겠다는 뜻입니다. 이렇게 강한 참조는 앞에서 설명한 것처럼 레퍼런스 카운트를 증가/감소 시키고 대상 객체에 대한 레퍼런스를 유지하며 위에서와 같은 순한 레퍼런스 문제가 발생할 수 있습니다.

반면에 약한 참조(weak)는 대상 객체에 대해 레퍼런스 카운트를 변화시키지 않습니다. 레퍼런스 카운트를 증가 시키지 않으면서 대상 객체를 참조할 수 있다는 뜻입니다. 위의 예에서 Child의 myParent 프로퍼티를 약한 참조로 선언하게 되면 순환 참조가 발생하지 않게 됩니다.

fig

unowned 는 기본적으로 weak 처럼 대상 객체의 레퍼런스 카운트에 영향을 주지 않으면는 참조를 만듭니다. 다만 다른 점은 대상 객체가 메모리에서 해제될 때  weak 의 경우에는 nil로 설정되지만 unowned 의 경우에는 값의 변화가 없습니다.

한 가지 중요한 점은 강한 참조의 경우는 대상 객체에 대한 소유권을 가지는 것과 같아서 레퍼런스를 유지하는 한 대상 객체의 레퍼런스 카운트가 0이 될 수 없어 메모리에서 해제되지 않습니다. 반면 약한 참조의 경우에는 소유권을 가지지 않고 단순히 대상 객체를 참조하는 것이므로 대상 객체는 레퍼런스 카운트가 0이 되는 순간 메모리에서 삭제됩니다. 이때 이 객체를 참조하고 있던 약한 참조 변수는 자동으로 nil이 되어버립니다. 따라서 약한 참조는 옵셔널 변수에만 가능하다는 뜻입니다.

반면에 unowned 키워드로 선언하게 되면 ARC 시스템이 대상 객체가 메모리에서 해제될 때 nil로 변경하지 않습니다. 따라서 unowned의 경우에는 옵셔널이 될 수 없습니다.

weak 변수는 반드시 옵셔널이어야합니다. ARC 시스템이 대상 객체의 메모리를 해제한 후에 해당 변수를 nil로 설정해야하기 때문입니다.

unowned 변수는 반드시 옵셔널이 아니어야 합니다. ARC 시스템이 nil로 설정하지 않기 때문입니다.

weak와 unowned 모두 레퍼런스 카운트를 증가시키지 않으면서 대상 객체에 대한 참조가 가능하다는 점은 동일하지만 대상 객체가 메모리에서 해제되었을때 weak의 경우에는 ARC에 의해 nil로 설정되므로 대상 객체가 더 이상 유효하지 않다는 것을 인지할 수 있습니다. 반면에 unowned의 경우에는 존재하지 않는 메모리를 참조하게 되는 문제가 발생할 수 있습니다.

클로저와 순환참조

두 개의 클래스 인스턴스의 프로퍼티가 서로 상대를 강한(strong) 참조를 하게 될 때 순환 참조 문제가 생기는 것을 위에서 보았습니다. 또한 약한(weak) 참조와 비소유(unowned) 참조를 통해서 순환 참조 고리를 끊을 수 있는 방법에 대해서도 알아 봤습니다.

클로저는 (함수와 클로저 표현식 모두를 말합니다) 레퍼런스형이므로 메모리관리의 대상입니다.

클로저 표현식을 클래스의 인스턴스의 프로퍼티에 대입하여 강한 참조를 만들고 클로저 본문에서 이 인스턴스에 대해 클로저 캡쳐하면 순환 참조가 발생하게 됩니다. 클로저 표현식 본문의 코드에서 인스턴스 자체나 그 인스턴스의 다른 프로퍼티를 self.someProperty와 같이 참조 하거나 self.someMethod()와 같이 메서드를 참조하게 되면 해당 대상에 대한 클로저 캡쳐가 발생하게 되는 것입니다. 즉, self를 캡쳐하게 되며 self와 클로저 간의 강한 순환 참조가 만들어 진다는 뜻입니다.

이러한 문제를 해결하기 위해서는 클로저 캡쳐 리스트 (closure capture list)를 사용해야 합니다. 캡쳐 리스트에 대한 이해를 하기 위해서 먼저 클로저를 사용하는 강한 순환 참조 상황을 만들어 보겠습니다.

아래의 예에서 HTML 문서의 개별 엘리먼트를 나타내는 HTMLElement 클래스를 정의하였습니다.

class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: Void -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

HTMLElement 클래스에서 “h1”, “p”, “br”과 같은 엘리먼트의 이름을 나타내도록 name프로퍼티를 정의하였습니다. text프로퍼티에는 HTML 엘리먼트의 텍스트 내용을 저장할 것입니다. 세 번째는 지연 프로퍼티로(lazy propertyasHTML를 정의하였습니다. asHTML 프로퍼티는 name과 text를 HTML 문자열 형태로 조합하여 리턴하는 클로저형의 프로퍼티입니다. 이 클로저는 별도의 입력없이 String을 반환하는 ()->String입니다.

asHTML이 지연 프로퍼티로(lazy property) 선언되었기 때문에 클로저 본문 코드에서 self를 참조할 수 있게 된 것입니다. 지연 프로퍼티는 인스턴스가 생성되고 초기화 과정이 완료된 시점에 self가 실제로 존재하는 시점 이전에는 호출 될 일이 없기 때문입니다. 위의 예제 코드에서 asHTML 프로퍼티의 lazy 키워드를 생략하면 self가 무엇인지 알 수 없다는 컴파일 에러가 발생하는 것을 확인할 수 있을 것입니다.

디폴트 초기값으로 asHTML 프로퍼티에는 HTML 태그 문자열을 리턴하는 클로저가 지정되었습니다. asHTML 프로퍼티는 이제 인스턴스 메서드 처럼 호출 될 수 있게 되었습니다. 하지만 asHTML은 인스턴스 메서드는 아니며 클로저 프로퍼티이므로 새로운 커스텀 클로저로 아래의 예와 같이 교체할 수 있습니다.

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// prints "<h1>some default text</h1>"

이제 새로운 HTMLElement의 인스턴스를 생성합니다.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints "<p>hello, world</p>"

여기서 paragraph 변수는 옵셔널 HTMLElement로 정의 하였고 아래에서 강한 순환 참조가 발생하는 것을 확인하기 위해서  nil로 설정하겠습니다.

아래 그림과 같은 강한 순환 참조가 디폴트 asHTML의 클로저와 HTMLElement 인스턴스 사이에 만들어 졌습니다.

fig

asHTML 프로퍼티는 클로저에 대한 강한 참조를 잡고 있으며 클로저 본문 내에서는 self를 참조함으로써 클로저 캡쳐하게 됩니다. 즉, HTMLElement 인스턴스로 강한 참조를 유지하게 됩니다. 이렇게 이 둘 사이에 강한 순환 참조가 발생하였습니다.

이제 paragraph 옵셔널 변수에 nil을 설정해서 HTMLElement 인스턴스에 대한 강한 참조를 끊어 보아도 인스턴스가 소멸되지 않는 것을 알 수 있습니다. 강한 순환 참조 때문입니다.

paragraph = nil

HTMLElement의 소멸자에 지정한 출력이 되지 않았으므로 인스턴스가 소멸되지 않았다는 것을 알 수 있습니다. 이제 프로그램에서 HTMLElement 인스턴스를 더 이상 참조할 수 있는 방법이 없음에도 인스턴스가 할당된 메모리를 해제 할 수 없으므로 메모리 누수가 발생하게 된 것입니다.

캡쳐 리스트 정의 방법

캡쳐 리스트의 각 각의 아이템은 weak나 unowned 키워드를 클래스 인스턴스의 레퍼런스(아래 예의 self) 또는 어떤 값에 의해 초기화 되는 변수(아래 예의 delegate = self.delegate)와 짝을 이뤄서 정의합니다.

캡쳐리스트는 클로저의 파라미터 리스트와 리턴형 앞에 대괄호로 감싸서 정의합니다.

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

만약 클로저에 파라미터와 리턴형이 지정되지 않았다면 캡쳐 리스트를 in 키워드 앞에 두면 됩니다.

lazy var someClosure: Void -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}

캡쳐 리스트

이제 위의 HTMLElement에서 발생한 강한 순화 참조 문제를 캡쳐 리스트를 활용하여 어떻게 해결 할 수 있는지 살펴보겠습니다.

asHTML 프로퍼티의 디폴트 클로저의 캡쳐리스트에 [unowned self]로 선언합니다.

class HTMLElement {
    let name: String
    let text: String?
    
    lazy var asHTML: Void -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

이전과 동일하게 인스턴스를 아래와 같은 코드로 생성합니다.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints "<p>hello, world</p>"

캡쳐 리스트로 인해 참조 구조가 아래 그림과 같이 달라집니다.

fig

이번에는 클로저에 의해 self가 캡쳐될때 strong대신에 unowned 레퍼런스로 캡쳐됩니다. 따라서 paragraph에 nil을 설정하면 정상적으로 인스턴스가 소멸되는 것을 확인할 수 있습니다.

paragraph = nil
// prints "p is being deinitialized"


[출처] https://outofbedlam.github.io/swift/2016/01/31/Swift-ARC-Closure-weakself/

'SWIFT' 카테고리의 다른 글

guard 문  (0) 2016.04.07
AlertView 띄우기 (iOS7,8)- Swift  (0) 2015.01.21
Swift Singletons 구현 방법  (0) 2014.12.09
Apple Swift Programming Language for KOREAN - 배포중  (0) 2014.06.12