^(코딩캣)^ = @"코딩"하는 고양이;

[번역글] Swift 클로저 사용을 위한 궁극의 가이드(3) [完]

Language/Objective-C & Swift
2021. 9. 20. 21:30

본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다.

Swift 클로저 사용을 위한 궁극의 가이드

이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다.

여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다.

이 튜토리얼에서는 다음과 같은 것을 설명할 것입니다.

  • 클로저란 무엇이고 어떻게 사용할 것인가
  • 클로저를 선언하고 호출하는 구문
  • 클로저의 타입을 구성하고 해체하는 방법
  • “클로징 오버(closing over)”란 무엇이고 캡처 리스트(capture list)는 어떻게 사용할 것인가?
  • completion handler로서 클로저를 어떻게 사용할 것인가

준비되었다면 지금부터 시작하겠습니다.

 

Swift의 클로저와 캡쳐

클로저를 단순히 변수에 대입이 가능한 함수처럼 보는 것은 클로저의 진정한 의미에 부합되지 않습니다. 이전까지 필자가 클로저를 이렇게 표현해 온 것은 가장 중요한 개념인 “캡쳐(capture)”를 빼고 설명해 온 것입니다.

“클로저(closure)”라는 이름은 “동봉하다(enclosing)”라는 말에서 따온 것인데, 좀 더 확장해 보면 이 단어는 함수형 프로그래밍의 개념인 “closing over”에서 온 단어입니다. Swift에서 클로저는 자신을 감싸고 있는 범위 내의 변수와 상수들을 캡쳐합니다(참고: ‘closing over’, ‘capturing’, ‘enclosing’은 여기에서 모두 같은 의미로 쓰였습니다).

Swift에서 모든 변수, 함수 및 클로저들은 스코프를 가졌습니다. 스코프(scope)는 여러분이 어디에서 특정 변수, 함수 또는 클로저에 접근할 수 있는지 그 범위를 결정해줍니다. 스코프 내에 변수, 함수 또는 클로저가 없는 경우 여러분은 대상에 접근할 수 없습니다. 스코프는 때떄로 맥락 또는 “컨텍스트(context)”라고도 부릅니다.

스코프를 여러분의 집 열쇠가 어디에 있을 지에 대한 컨텍스트와 비교해 보겠습니다. 여러분이 집 밖에 나가 있을 때 집 열쇠도 집 밖에 있을 것입니다. 여러분은 집 밖에 나와 있는데 열쇠는 집 안에 있다면 여러분과 열쇠는 같은 컨텍스트 내에 있는 것이 아닙니다. 따라서 여러분은 현관문을 열 수 없습니다. 여러분과 집 열쇠가 모두 집 밖에 나와 있어야 같은 컨텍스트에 있는 것이므로, 여러분은 현관문을 열 수 있습니다.

여러분의 코드는 글로벌 스코프(global scope)와 로컬 스코프(local scope)로 나눌 수 있습니다.

클래스에 정의된 프로퍼티는 글로벌 스코프의 일부입니다. 클래스 내 어느 곳에서든 여러분은 프로퍼티의 값을 가져오거나 설정할 수 있습니다.

함수나 메소드 안에서 정의된 변수는 로컬 스코프입니다. 그 함수 범위 안에서만 여러분이 값을 가져오거나 설정할 수 있습니다.

 

클로저가 자신을 감싸고 있는 스코프를 어떻게 캡쳐하는지 예제로 확인해 보겠습니다.

// Swift
let name = "Zaphod"
let greeting = {
    print("Don't panic, \(name)!")
}

greeting()

(1) name이라 이름 붙은 상수는 문자열인 "Zaphod"를 보관하고 있습니다.

(2) greeting이라 이름 붙은 상수에 클로저가 정의되고 보관됩니다. 이 클로저는 메시지를 출력합니다.

(3) 마지막 줄에서 클로저가 호출됩니다.

 

위 예에서 클로저는 지역 변수인 name을 캡처(close over)합니다. 다시 말하면 클로저는 자신이 정의된 스코프 내에서 접근 가능한 변수들을 캡슐화합니다. 그 결과 우리는 클로저 안에서 지역적으로 선언되지 않은 식별자인 name을 클로저 안에서 사용할 수 있습니다.

 

다음에 제시될 좀 더 복잡한 예를 보겠습니다.

// Swift
func addScore(_ points: Int) -> Int {
    let score = 42
    let calculate = {
        score + points
    }
    
    return calculate()
}

let value = addScore(11)
print(value)

(1) addScore(_:)가 정의됩니다. 이것은 매개변수 point로 넘어온 값에 이전 점수인 42를 더한 값을 반환합니다.

(2) 이 함수 안에는 클로저 calculate가 정의되어 있습니다. 이것은 단순히 scorepoint를 더하여 그 값을 반환합니다. 이 함수는 calculate()로써 클로저를 호출합니다.

(3) addScore(_:)가 호출되고 그 결과를 보관한 뒤 출력합니다.

 

클로저 calculate는 두 개의 값인 scorepoints를 캡쳐합니다. 이들 변수는 둘 다 클로저 안에서는 선언된 적이 없지만 클로저는 문제없이 이들의 값을 가져올 수 있습니다. 클로저가 자신의 스코프 내 변수들을 캡쳐했기 때문입니다. 클로저의 캡쳐에 대해 몇 가지 주목할만한 사항은 다음과 같습니다.

(1) 클로저는 자신의 내부에서 실제로 사용하는 변수 등을 캡쳐합니다. 클로저 내에서 접근되지 않는 변수 등은 캡쳐되지 않습니다.

(2) 클로저는 변수와 상수를 캡쳐할 수 있습니다. 물론 기술적으로 클로저도 변수나 상수의 일종이기 때문에 다른 클로저를 캡쳐할 수도 있습니다. 또한 프로퍼티의 경우 변수 또는 상수에 대입되어 있는 어떤 객체에 소속된 프로퍼티이기 때문에 여러분은 이 또한 클로저로 캡쳐할 수 있습니다. 물론 클로저 안에서 클로저 밖에 있는 함수(메소드)를 호출할 수도 있습니다. 함수(메소드)는 변수 또는 상수도 아니고 여기에 보관되는 개념도 아니기에 캡쳐되지는 않지만 호출이 됩니다.

(3) 캡쳐는 일방향성을 갖습니다. 클로저는 자신이 정의된 스코프 내의 것들을 캡쳐하는데, 클로저 바깥에서는 클로저 안에서 선언 및 정의된 사항들에 대해서 접근할 수 없습니다.

 

한편 클로저로 값을 캡쳐하는 것은 strong 참조의 순환이나 메모리 누수로 이어질 수 있습니다. 그래서 ‘캡쳐 리스트(capture list)’가 도입되게 되었습니다.

 

Strong 참조와 캡쳐 리스트

클로저는 값들을 캡쳐하는데 이 때 그 값에 대한 strong 참조를 자동으로 생성합니다. 예를 들어 Bob이 Alice에 대해 strong 참조를 가지고 있을 때 Bob이 메모리에서 제거되지 않는 한 Alice도 메모리에서 제거되지 않습니다.

여기까지는 좋습니다. 하지만 Alice가 Bob에 대해 strong 참조를 가지고 있다면 어떻게 될까요? Bob과 Alice는 서로 붙잡고 있기 때문에 둘 다 메모리에서 제거될 수 없습니다. 이것을 string 참조 순환이라 하며 메모리 누수의 원인이 됩니다. 만일 Bob과 Alice가 10MB씩 차지하고 있다면 다른 앱들은 그 만큼의 메모리를 사용 못하게 되고 이를 제거할 방법도 없게 됩니다.

캡처 리스트를 사용하면 strong 참조 순환을 깰 수 있습니다. 클래스의 프로퍼티에 weak 키워드를 붙인 것과 같이 클로저가 캡쳐할 값에 weakunowned 참조를 표시할 수 있습니다. 이를 명시하지 않는다면 기본 속성인 strong이 지정됩니다.

// Swift
class Database {
    var data = 0
}

let database = Database()
database.data = 11010101

let calculate = { [weak database] multiplier in
    database.data * multiplier
}

let result = calculate(2)
print(result)

(1) data라는 프로퍼티 하나를 갖는 Database 클래스를 선언 및 정의하고 이에 대한 새 인스턴스를 생성합니다. 그리고 data 프로퍼티에 어떤 정수 값을 설정합니다.

(2) calculate 클로저를 정의합니다. 클로저는 하나의 매개변수인 multiplier를 받습니다. 또한 database를 캡쳐합니다. 클로저 안에서 database.data 값은 multiplier로 곱해진 다음 반환됩니다.

(3) 마지막 줄에서 클로저는 2라는 매개변수를 가지고 호출된 다음 그 결과가 result에 보관됩니다.

 

여기서 중요한 부분은 다음과 같은 캡쳐 리스트입니다.

// Swift
... { [weak database] ... } ...

캡쳐 리스트는 컴마로 구분되는 변수 이름의 목록인데 대괄호로 둘러싸여서 weak 또는 unowned 키워드가 붙을 수 있습니다.

// Swift
[weak self]
[unowned navigationController]
[unowned self, weak database]

여러분은 strong 순환 참조를 깨기 위해 위와 같이 weak 또는 unowned 키워드로써 캡쳐 리스트를 사용할 수 있습니다.

(1) weak 키워드는 캡쳐된 값이 nil이 될 수 있음을 지시합니다.

(2) unowned 키워드는 캡쳐된 값이 절대 nil이 될 수 없음을 지시합니다.

 

weakunowned 키워드는 둘 다 strong 참조의 반대 개념입니다. 다만 weak는 캡쳐된 변수가 nil을 참조할 수 있다는 것이 unowned와의 차이입니다.

통상적으로 클로저와 캡쳐 값이 항상 서로 참조하고 있으며 동시에 소멸될 때 unowned 키워드를 사용합니다. 예를 들어 뷰 컨트롤러에서 [unowned self]가 있는데, 이 경우 클로저는 뷰 컨트롤러보다 오래 존속될 수 없습니다.

여러분은 또한 어떤 경우 캡쳐 값이 nil이 될 수 있을 때 weak를 사용할 수 있습니다. 주로 클로저가 자신을 생성해 준 컨텍스트보다 더 오래 존속될 수 있을 때 이것을 사용할 수 있는데, 예를 들어 오래 걸리는 작업이 완료되기 전에 뷰 컨트롤러가 소멸될 수 있을 경우가 해당됩니다. 결과적으로 캡쳐된 값은 옵셔널 타입입니다.

캡쳐, 캡쳐 리스트 및 메모리 관리의 개념은 어려울 수 있겠으나 그렇게 복잡한 것만은 아닙니다. 추상적인 개념을 설명하기가 참으로 어려운 일이지만, 실무 iOS 개발에서 일반적으로 캡쳐 리스트는 [weak self]이나 [unowned self]와 같이 작성합니다.

여기까지 살펴본 내용을 정리하면 다음과 같습니다.

(1) 클로저는 자신을 감싸는 스코프를 캡쳐할 수 있습니다. 그래서 해당 스코프 내의 변수와 상수들은 클로저 안에서 접근이 가능합니다.

(2) 변수와 상수는 기본적으로 strong 참조로 캡쳐되는데 이는 strong 참조 순환을 야기할 수 있습니다.

(3) 여러분은 캡쳐 리스트를 사용하여 strong 순환 참조 문제를 깰 수 있습니다. 예를 들어 weakunowned로 명시될 수 있습니다.

 

클로저에 대한 내용은 여기까지입니다.

카테고리 “Language/Objective-C & Swift”
more...