본문 바로가기

Swift

[Swift] Capturing Values 값을 캡쳐한다는 것

Swift에서 값을 캡쳐(Capture)한다는 것의 의미를 한 번 알아보겠습니다. 클로저(Closure)는 정의된 주변의 컨텍스트에 있는 상수나 변수들을 캡처할 수 있습니다. 그런 다음 클로저는 상수와 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도 그 상수와 변수의 값을 참조하고 수정할 수 있습니다.

 

스위프트에서 값을 캡처할 수 있는 클로저의 가장 간단한 형태는 중첩 함수(Nested Function)로서, 다른 함수의 body 안에 정의되어 있습니다. 중첩 함수는 외부 함수의 인수를 캡처할 수 있으며, 또한 외부 함수 내에 정의된 상수와 변수를 캡처할 수 있습니다.

 

incrementer 중첩 함수를 포함한 makeIncrementer 함수 예제를 살펴보겠습니다. incrementer 함수는 runningTotalamount 값을 캡처하고 있습니다. 이 값들을 캡처한 후에, incrementer 함수는 호출될 때 마다 runningTotalamount 값을 더하여 반환하는 클로저로서 makeIncrementer 의 반환 값으로 사용되고 있습니다.

 

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount // runningTotal과 amount를 캡처
        return runningTotal
    }
    return incrementer
}

 

makeIncrementer 함수는 () -> Int 이 반환 타입입니다. 이것은 간단한 값이 아닌 함수를 반환한다는 것을 의미합니다. 반환되는 함수는 매개변수가 없으며, 호출될 때마다 Int 값을 반환함을 알 수 있습니다.

중첩 함수가 중첩되어있지 않다고 생각해보면 매우 이상하게 보일 것입니다.

 

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

 

incrementer 함수에 아무런 매개변수가 없기 때문에 runningTotalamount 는 참조 값을 캡처한 것 임을 알 수 있습니다. 참조 값을 캡처한다는 것은 makeIncrementer 함수가 종결되어도, incrementer 함수가 다음에 또 호출되더라도 runningTotalamount 는 사라지지 않는다는 것을 보장합니다.

 

스위프트는 해당 값이 클로저에 의해 변형 되지 않고, 클로저가 생성된 후에 값이 변형 되지 않는 경우에 값의 복사본을 캡처하여 저장할 수 있도록 최적화되어 있습니다.

 

또한, 스위프트는 변수가 더 이상 필요하지 않을 때 필요한 처리를 포함한 모든 메모리 관리를 다룹니다.

 

makeIncrementer 함수를 사용해보겠습니다.

 

let incrementByTen = makeIncrementer(forIncrement: 10)

 

incrementByTen 상수에는 runningTotal 을 10씩 증가시켜 해당 값을 반환하는 함수가 할당되어졌을 것입니다.

 

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

 

만약에 새로운 incrementer 를 생성한다면, 새로운 runningTotal 변수가 생성될 것입니다.

 

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

 

이후에 다시 incrementByTen 함수를 호출하면 어떻게 될까요?

 

incrementByTen()
// returns a value of 40

 

incrementByTenrunningTotalincrementBySevenrunningTotal 은 서로 다른 참조이기 때문에 서로에게 전혀 영향을 주지 않습니다.

 

클로저는 참조 타입(Reference Type)입니다. 이것은 해당 클로저를 다른 상수나 변수에 할당해도 같은 클로저를 참조하고 있다는 의미입니다.

 

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

 

incrementByTenalsoIncrementByTen 은 같은 클로저를 참조하기 때문에 runningTotal 값을 같이 증가시킵니다.

주의할 점

만약 클래스 인스턴스의 프로퍼티로 클로저를 할당하고 해당 인스턴스 또는 해당 인스턴스의 멤버 변수들을 참조하여 해당 인스턴스를 캡처하는 경우, 클로저와 인스턴스 사이에 강한 참조 사이클(Strong Reference Cycle)이 발생합니다. 스위프트는 캡처 목록(Capture Lists)을 이용하여 강한 참조를 해제합니다. 강한 참조 사이클은 메모리 관리 관점에서 매우 중요한 내용으로 이에 대해서는 ARC에 대한 포스팅에서 자세히 다루겠습니다.

정리

  • 값을 캡처하는 가장 간단한 형태는 중첩 함수(Nested Funcion) 입니다.
  • 함수 내부의 값을 캡처하면 함수가 종결되어도 해당 값을 참조할 수 있습니다.
  • 인스턴스를 캡처하게 되면 강한 참조가 발생되어 메모리 관리 이슈가 발생합니다.

태그