Swift로 클로저 사용 시 메모리 관리 주의사항

작성일 :

Swift로 클로저 사용 시 메모리 관리 주의사항

클로저는 Swift에서 매우 유용한 기능이지만, 올바르게 사용하지 않으면 메모리 관리 문제를 일으킬 수 있습니다. 특히 클로저와 관련된 강한 참조 사이클(strong reference cycle)은 메모리 누수를 초래할 수 있으며, 이는 앱 성능에 부정적인 영향을 미칩니다. 이 글에서는 클로저 사용 시 주의해야 할 메모리 관리 문제와 이를 해결하는 방법에 대해 설명합니다.

클로저란 무엇인가?

클로저는 코드 블록을 정의하고 실행 컨텍스트 하에서 작성된 함수 또는 메소드입니다. 클로저는 변수와 상수의 캡처 및 저장이 가능하며, 다양한 방식으로 활용될 수 있습니다. 예를 들어, 클로저는 콜백 함수로 사용되거나 변수처럼 전달될 수 있습니다.

swift
let greetingClosure = { (name: String) -> String in
    return "Hello, \(name)!"
}

print(greetingClosure("World")) // Output: Hello, World!

위 예제는 간단한 클로저의 예시로, 전달된 이름을 사용해 인사말을 반환합니다. 이처럼 클로저는 매우 유연하게 사용될 수 있지만, 메모리 관리에 신경 쓰지 않으면 문제가 발생할 수 있습니다.

강한 참조 사이클 문제

강한 참조 사이클은 클로저가 자신의 실행 컨텍스트 내의 변수를 강한 참조로 캡처하는 경우 발생합니다. 이런 상황은 메모리 누수를 초래하여 앱의 메모리 사용량을 증가시키고 성능을 저하시킬 수 있습니다.

swift
class Person {
    var name: String
    var greeting: (() -> String)?
    
    init(name: String) {
        self.name = name
        self.greeting = { [unowned self] in
            return "Hello, \(self.name)"
        }
    }

    deinit {
        print("
할당 해제 됨: \(name)
"
)
    }
}

var john: Person? = Person(name: "John")
print(john?.greeting?()) // Output: Hello, John!
john = nil // 이 시점에서 deinit 호출

이 코드에서 Person 클래스는 자신의 이름을 사용해 인사말을 반환하는 클로저를 가지고 있습니다. 클로저 내에서 self를 강한 참조로 캡처하지 않으면 메모리 누수가 발생하지 않지만, 그렇지 않으면 Person 인스턴스는 해제되지 않습니다.

약한 참조와 미소유 참조

강한 참조 사이클 문제를 해결하기 위해 Swift는 약한 참조(weak)와 미소유 참조(unowned) 키워드를 제공합니다. weak 참조는 참조 대상이 해제되면 자동으로 nil로 설정되며, unowned 참조는 참조 대상이 해제되면 접근할 때 런타임 오류를 발생시킵니다.

swift
class House {
    var owner: Person?

    init() {}

    deinit {
        print("House deinitialized")
    }
}

let createRelationship = {
    let house = House()
    let person = Person(name: "Alice")
    house.owner = person
    person.greeting = { [weak person] in
        return "Hello, \(person?.name ?? "Unknown")"
    }
}

createRelationship()
// 이 시점에서 House와 Person 인스턴스는 모두 해제됩니다.

위 예제에서 클로저 내에 있는 person 참조는 약한 참조로 설정되어, 강한 참조 사이클을 방지합니다. 따라서 createRelationship 호출 후에는 HousePerson 인스턴스가 제대로 해제됩니다.

미소유 참조 사용 시 주의사항

unowned 키워드는 참조 대상이 반드시 해제되지 않도록 보장할 수 있을 때만 사용해야 합니다. 만약 참조 대상이 해제되면 런타임 오류가 발생하므로 주의가 필요합니다. 아래 코드는 unowned 참조의 사용 예시입니다.

swift
class Employer {
    var employee: Employee!
    
    init() {
        self.employee = Employee(manager: self)
    }
    deinit {
        print("Employer deinitialized")
    }
}

class Employee {
    unowned let manager: Employer
    
    init(manager: Employer) {
        self.manager = manager
    }

    deinit {
        print("Employee deinitialized")
    }
}

var employer: Employer? = Employer()
employer = nil // 이 시점에서 Employer와 Employee는 모두 해제됩니다.

위 코드에서 Employee 클래스는 Employer를 미소유 참조로 가지고 있어, 두 인스턴스 간의 강한 참조 사이클을 방지합니다. employer 변수가 nil로 설정되면 두 인스턴스는 모두 해제됩니다.

결론

Swift에서 클로저를 사용할 때 메모리 관리 문제를 피하기 위해서는 약한 참조와 미소유 참조를 적절히 사용해야 합니다. 강한 참조 사이클을 방지함으로써 메모리 누수를 최소화하고, 앱 성능을 유지할 수 있습니다. 클로저 사용 시 이러한 주의사항을 염두에 두어 안전하고 효율적인 코드를 작성하시기 바랍니다.