최근 프로젝트의 기능구현이 끝난후 테스트를 하다가 앱이 점점 느려지는 상황을 마주했습니다. 즉시 디버그 세션을 확인해보니 예상대로 메모리가 점점 치솟고 있었습니다

기능 구현이 끝난후여서 장황하게 늘어진 코드속에서 적지 않은 시간을 소비하여 수정을 하다 '중간중간 확인해 주었다면 이렇게 오래걸리지 않았을텐데' 라는 생각이 들었습니다. 때문에 swift의 Memory 관리가 어떻게 작동되는지, Retain Cycle이 무엇인지 그리고 해결방법에 대해 포스팅하겠습니다.


• Memory Management


스위프트는 Automatic Reference Counting(ARC)를 사용하여 메모리를 추적 및 관리를 합니다.

개념적으로 아주 간단합니다.클래스 인스턴스가 참조 및 해제할때마다 카운팅을하며 strong reference가 0이 됬을때 메모리에서 해제되는 방식입니다

아래 코드로 ARC의 동작을 볼수 있습니다.


class Person {

    static var ARC = 0

    init() {

        Person.ARC += 1

        print("Person is maked. \(Person.ARC)")
    }

    deinit {

        Person.ARC -= 1

        print("Person is deleted. \(Person.ARC)")
    }
}


var papa: Person? = Person() // Person is maked. 1

var mama: Person? = Person() // Person is maked. 2

papa = nil // Person is deleted. 1

mama = nil // Person is deleted. 0


• Retain Cycle


메모리가 해제되지 않고 유지되어 누수가 생기는 현상을 말합니다.

아래의 코드는 두 개의 객체가 상호 참조하여 강한 순환 참조가 생겨 메모리가 해제되지 않는 모습입니다.


class Person {

    static var ARC = 0

    var spouse: Person?

    init() {

        Person.ARC += 1

        print("Person is maked. \(Person.ARC)")
    }

    deinit {

        Person.ARC -= 1

        print("Person is deleted. \(Person.ARC)")
    }
}


var papa: Person? = Person() // Person is maked. 1

var mama: Person? = Person() // Person is maked. 2

papa?.spouse = mama

mama?.spouse = papa


papa = nil

mama = nil


객체에 nil을 할당하여 메모리가 해제되어야하는데 해제가 되지 않는것을 deinit 로그가 찍히지 않는것으로 확인하실 수 있습니다.

좀 더 직관적인 이해를 위해 그림을 준비하였습니다.



papa 와 mama 의 프로퍼티인 spouse 가 서로를 참조하고 있으므로 ARC가 0이 되지 않아 메모리에서 해제가 되지않았습니다.

더 심각한것은 papa 와 mama 인스턴스에 nil을 할당하여 더 이상 참조에 대해 접근이 불가능하여 해결방법은 없습니다.


그럼 다시 돌아가서 해결 방법에 대해 알아보겠습니다.


제가 위에서 "강한"이라고 말한것이 힌트입니다.

객체에대한 레퍼런스를 정의할때 strong, weak, unowned 3가지 방법이 있습니다. 그중 strong이 default이기 때문에 "강한" 순환참조인 것입니다.


타입명에서 느껴지듯이 해결 방법은 강함의 반대임 약함을 사용하면 해결이 됩니다.


약한참조(weak)는 객체의 레퍼런스 카운트를 변화하시키지 않습니다. 약한 참조만이 남아있다면 객체는 메모리를 해제하며 변수는자동으로 nil이됩니다. 때문에 weak은 반드시 optional 타입이어야합니다.


그리고 미소유참조(unowned)는 weak 처럼 카운트를 변화시키지 않지만 항상 값이 있음을 가정하기 때문에 옵셔널 타입이 아닙니다. 여기서 주의할점은 메모리가 해제되어있음에도 가정때문에 접근한다면 runtime exception 오류가 발생해 앱이 crash가 될것입니다. 때문에 해당 변수가 가리키는 객체의 메모리가 해제된 이후에는 해당 영역을 가리키지 않는다는 확신이 있을때 사용하는것이바람직합니다.


class Person {

    static var ARC = 0

    weak var spouse: Person?


    init() {

        Person.ARC += 1

        print("Person is maked. \(Person.ARC)")
    }


    deinit {

        Person.ARC -= 1

        print("Person is deleted. \(Person.ARC)")
    }
}


var papa: Person? = Person() // Person is maked. 1

var mama: Person? = Person() // Person is maked. 2

papa?.spouse = mama

mama?.spouse = papa


papa = nil // Person is deleted. 1

mama = nil // Person is deleted. 0



다음으로 클로저를 사용할때 일어나는 순환참조에 대해 알아보겠습니다.


클로저는 바깥의 값을 사용할 때는 값을 복사하여 클로저 내부에 저장하고 사용하는 것이 아닌 해당 값을 참조하여 사용하게 됩니다.

때문에 아래의 코드는 강한순환참조 문제가 발생하고 있습니다.



class Person {

    static var ARC = 0

    var name: String

    var age: Int

    lazy var Info: () -> String = {

        return "\(self.name) is \(self.age) years old"

    }

    init(name: String, age: Int) {

        self.name = name

        self.age = age

        Person.ARC += 1

        print("Person is maked. \(Person.ARC)")
    }

    deinit {

        Person.ARC -= 1

        print("Person is deleted. \(Person.ARC)")
    }
}

var papa: Person? = Person(name: "ian", age: 27) // Person is maked. 1

print(papa?.Info()) // Optional("ian is 27 years old")

papa = nil


여기서 지연변수(lazy)를 사용한 이유는 인스턴스가 생성되고 초기화 과정이 완료된 시점에 self가 실제로 존재하는 시점의 값을 참조할 수 있게 만들기 위함입니다. (lazy를 사용하지 않으면 컴파일 에러가 생깁니다.)


info 프로퍼티는 클로저에 대한 강한 참조를 하고 있으며 클로저 블록 내에서는 self를 참조하고있습니다.

때문에 papa 변수의 Person클래스의 참조 카운트가 nil로 인해 사라져도 여전히 카운트는 0 이 되지않아 메모리해제가 이루어지지 않습니다.


이러한 상황의 해결방법 또한 위에서 말씀드린 weak , unowned를 사용하여 해결할 수 있습니다.


class Person {

    static var ARC = 0

    var name: String

    var age: Int


    lazy var Info: () -> String = { [unowned self] in

        return "\(self.name) is \(self.age) years old"
    }

    init(name: String, age: Int) {

        self.name = name

        self.age = age

        Person.ARC += 1

        print("Person is maked. \(Person.ARC)")
    }

    deinit {

        Person.ARC -= 1

        print("Person is deleted. \(Person.ARC)")
    }
}

var papa: Person? = Person(name: "ian", age: 27) // Person is maked. 1

print(papa?.Info()) // Optional("ian is 27 years old")

papa = nil // Person is deleted. 0


클로저 블록 내에서 self를 미소유 참조를하여 info 프로퍼티와 self 사이의 강한참조가 이루어지지 않게 만들어주었습니다.


마지막으로

클로저를 지역적으로 사용한다면 클래스와 클로저변수 자체가 강한참조가 아니므로 레퍼런스타입을 사용하지 않아도됩니다.


class Person {


    static var ARC = 0

    var name: String

    var age: Int

    init(name: String, age: Int) {

        self.name = name

        self.age = age

        Person.ARC += 1

        print("Person is maked. \(Person.ARC)")

    }

    deinit {

        Person.ARC -= 1

        print("Person is deleted. \(Person.ARC)")

    }

    func getInfo() -> String {

        return {

            return "\(self.name) is \(self.age) years old"
        }()
    }
}

var papa: Person? = Person(name: "ian", age: 27) // Person is maked. 1

print(papa?.getInfo()) // Optional("ian is 27 years old")

papa = nil // Person is deleted. 0




이상.