이름있는 클로저 사용 시 주의점

권승용(Eric)·2024년 12월 5일

TIL

목록 보기
18/38

배경

  • 클로저의 값 캡처 예시로 아래와 같은 예제를 다뤘다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
  • 문제는 named closure인 incrementer에는 문법적으로 캡처 리스트를 활용할 수 없다는 것이다.
  • 캡처 리스트를 활용해 독립적인 값의 복사본을 가지고 싶다면 아래와 같이 코드를 수정할 수 있다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    let runningTotal = 0
    let incrementer = { [runningTotal, amount] in
        let result = runningTotal + amount
        return result
    }
    return incrementer
}
  • 다만 이 코드는 이전 코드와 다른 결과를 보인다.
  • 캡처 리스트로 인해 immutable한 값의 복사본을 가지게 되어 각각의 덧셈 연산이 독립적으로 시행되어, 반환되는 incrementer를 여러 번 수행해도 같은 값을 반환한다.
  • 유의해야 할 점은, 캡처 리스트를 통해 레퍼런스 카운트를 감소시키고 싶은 경우에 named closure를 사용하면 안 된다는 것이다.
  • 그 경우 캡처 리스트를 사용하지 못하는 상황이지만 캡처는 발생하고, 컴파일러는 별다른 오류를 보여주지 않을 수도 있다.

func 사용 시 메모리 누수 예제

class SomeClass {
    var someClosure: () -> Void
    var someVariable = 0
    
    init() {
        self.someClosure = {}
        self.someClosure = someFunction
        print("init")
    }
    
    func someFunction() {
        someVariable -= 1
    }
    
    deinit {
        print("deinit")
    }
}

var someClass: SomeClass? = SomeClass()
someClass = nil
  • 위와 같은 코드를 실행한 결과는 아래와 같다.
  • someClosure에 할당된 someFunction 내부의 self 사용 때문에 순환 참조가 발생해, deinit이 호출되지 않는다.
  • 동작 자체는 문제없지만, 컴파일 단계에서 최소한의 검증도 거치지 못한다는 점이 문제가 된다.
  • 일반적인 이름 없는 클로저를 탈출 클로저로 사용할 때엔 self를 명시적으로 작성해 주어야 한다.
class SomeClass {
    var someClosure: () -> Void
    var someVariable = 0
    
    init() {
        self.someClosure = {
            someVariable += 1
        }
        print("init")
    }
    
    deinit {
        print("deinit")
    }
}

var someClass: SomeClass? = SomeClass()
someClass = nil
  • 위와 같이 익명 탈출 클로저 작성 시에는 아래와 같은 에러를 마주할 수 있다.
  • 탈출 가능성이 있는 클로저에서 자기 자신의 클래스 인스턴스를 참조할 땐, self를 명시적으로 작성해 캡처됨을 알려야 한다.
  • 이같은 컴파일 에러는 코드를 읽는 사람으로 하여금 해당 코드는 순환 참조의 위험이 있음을 인지하게 한다.
  • 그러나 func을 사용한 이름 있는 함수는 이러한 컴파일 에러에서 비껴가면서도 캡처 리스트를 사용할 수 없기 때문에, 순환 참조 가능성을 가지면서도 쉽게 알아채기 힘들 수 있다.
  • 따라서 웬만하면 탈출 클로저는 익명 클로저로 작성하는 것이 좋겠고, 순환 참조를 예방하기 위한 좋은 구조로 코드를 작성하는 것이 좋겠다.

출처

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures

profile
ios 개발자에용

0개의 댓글