[swift] 클로저 사용시 발생할 수 있는 문제점

클로저는 Swift에서 많이 사용되는 강력한 기능 중 하나입니다. 클로저를 사용하면 코드의 재사용성과 유연성을 높일 수 있습니다. 하지만 클로저를 사용할 때 발생할 수 있는 몇 가지 문제점도 주의해야 합니다. 이번 글에서는 클로저 사용 시 주의해야 할 문제점에 대해 알아보겠습니다.

1. Strong Reference Cycle

클로저 내부에서 클래스나 구조체에 접근하면서 클로저와 해당 객체 사이의 강한 참조 순환(Strong Reference Cycle)이 발생할 수 있습니다. 이는 메모리 누수를 발생시키는 원인이 됩니다. 이런 상황을 방지하기 위해서는 [unowned self]나 [weak self]를 사용하여 클로저 내부에서 객체를 강한 참조하는 것을 방지해야 합니다.

class MyClass {
    var closure: (() -> Void)?
    
    func runClosure() {
        closure = {
            // self 참조 사용 시 주의
            self.doSomething()
        }
        closure?()
    }
    
    func doSomething() {
        print("Do something")
    }
}

위의 예시처럼 클로저 내부에서 self를 사용할 때는 주의해야 합니다. 만약 위의 예시에서 self를 unowned나 weak로 선언하지 않는다면, MyClass 인스턴스와 클로저 사이에 강한 참조 순환을 일으켜 메모리 누수가 발생합니다.

2. 경쟁 상태 (Race Condition)

클로저를 비동기 작업에 사용할 때, 여러 클로저가 동시에 실행되는 경우 경쟁 상태가 발생할 수 있습니다. 경쟁 상태는 여러 클로저가 동일한 변수에 동시에 접근하거나 수정할 때 발생합니다. 이를 방지하기 위해서는 동기화 메커니즘을 사용해야 합니다.

class MyClass {
    var count = 0
    let queue = DispatchQueue(label: "com.example.myqueue")
    
    func increment() {
        queue.async {
            self.count += 1
        }
    }
}

위의 예시에서는 DispatchQueue를 사용하여 클로저가 순차적으로 실행될 수 있도록 합니다. 이렇게 하면 여러 클로저가 count 변수에 동시에 접근하더라도 경쟁 상태를 방지할 수 있습니다.

3. Capture list 사용의 오해

클로저에서 외부 변수를 참조할 때, capture list를 사용하여 클로저에 대한 참조 방식을 정의할 수 있습니다. 하지만 capture list를 잘못 사용하는 경우 예상하지 못한 동작이 발생할 수 있습니다.

var closures = [() -> Void]()
func addClosure() {
    var value = 0
    let closure = { [value] in
        print(value)
    }
    closures.append(closure)
}

위의 예시에서는 클로저를 배열에 추가하려고 하는데, capture list를 잘못 사용하여 클로저가 value를 복사해오는 것이 아니라 참조하는 방식으로 동작하게 됩니다. 따라서 배열에 추가된 모든 클로저는 동일한 value 변수를 참조하게 되므로 마지막에 할당된 값만 출력됩니다.

결론

클로저는 강력한 기능이지만 사용할 때 주의해야 할 몇 가지 문제점이 있습니다. 메모리 누수와 경쟁 상태를 방지하기 위해 올바른 클로저 사용 방법을 숙지하고, capture list를 올바르게 사용하도록 주의해야 합니다. 이렇게 하면 안정적이고 효율적인 코드를 작성할 수 있습니다.

참고자료:
Apple Developer Documentation
Swift.org