Swift - 클로져(Closures)

클로져(Closure)는 현대적인 언어에서 상당히 집착하는 개념같다. 없어도 딱히 문제는 없겠지만 여러가지 면에도 기존 함수 방식의 단점을 극복 할 수 있기 때문인 것 같다.

어쨌거나 스위프트(Swift)도 클로져 개념을 지원한다. 문법이 함수(Function)와 비슷하기에 클로져를 이해하려면 함수에 관한 지식이 필요하다.

[관련글] Swift - 함수(Function)

클로져(Closure)

클로져는 함수와 비슷하다. 정확히 말하자면 이름이 없고 함수 내이든 어디든 만들 수 있는 함수 비스무리한 녀석이다. 축약하여 "이름없는 함수" 라고 할 수 있겠다. 혼히 "익명함수" 혹은 "람다함수" 라고도 불리지만...

개념적으로 보기에 함수(Function)와는 다르다는 것이 일반적이지만, 사실 클로져를 설명하기엔 함수라는 대체제가 필요하다.

일반적으로 함수는 전역 네임스페이스에 소속되는 정적인 구현체를 가진 코드 덩어리다. '뭐야 도데체 이말은' 이라는 욕이 나와도 그냥 넘어가 줬으면 좋겠다. 클로져의 개념 설명은 너무 어렵다. -_-;

확실한 것은, 클로져는 함수와는 다른 체제를 가지고 있다. 코드 내부에서 일시적으로 생성되어서 동작하는 함수와 비슷한 기능을 하는 코드 덩어리이고 이 함수에 전달되는 인자 역시 해당 시점에서 동적으로 복사되어서 전달된다. 그럼 동적 생성 함수라고 하면 비슷하려나?

설명하기에 너무 어렵다. 그냥 자연스럽게 체감하는 편이 좋을 것 같다.

예제

공식책(?)에서 클로져를 가장 잘 활용하는 예제로 Reversed Sort 를 꼽고 있기에 같은 예제를 한번 보자.
let names = [ "Chris", "Alex", "Ewa", "Barry", "Daniella" ]

var sorted = sort(names)
// sorted = ["Alex", "Barry", "Chris", "Daniella", "Ewa"]


func backwards(s1: String, s2: String) -> Bool {
    return s1 > s2
}

var reversed = sort(names, backwards)
// reversed = ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

reversed = sort(names, 
                { (s1: String, s2: String) -> Bool in return s1 > s2 } )
// reversed = ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
제일 처음 사용한 sort는 그냥 알파벳 순서대로 정렬한 것이다. 그리고 그 아래 reversed에 들어가게 되는 sort는 모두 역순으로 정렬을 한 것이다. 여기서 두 번째 정렬은 backwards 라는 함수를 별도로 정의해서 sort의 두 번째 인자의 비교함수로 집어넣어서 역순으로 정렬을 하게 된다. 만약 비교문을 반대(s1 < s2)로 적는되면 아마두 순서대로 정렬될 것이다.

세 번째 sort는 두 번째와 결과가 같지만 여기서 다루는 클로져라는 개념으로 비교함수를 대체한 것이다. 별도로 뽑아보면
{ (s1: String, s2: String) -> Bool in return s1 > s2 }
이렇게 생겼다.

모양을 보면 함수의 이름이 없다는 점만 빼면 함수와 구조가 비슷하다. 리턴 타입 앞에 -> 가 오는 것도 동일하다. 대신 함수 몸체에 해당하는 중괄호가 없고 in 이라는 커맨드 뒤에 함수의 내용이 오는 것을 알 수 있다.

이것이 클로져의 정체다. 어디서든 만들 수 있는 단순한 무명함수(?)다.

클로져 문법

공식책(?)에 정의된 스위프트의 클로져 문법은 이런 식이다.
그런데 스위프트에선 좀 더 단축된 문법을 제공하기도 한다.
reversed = sort(names, { s1, s2 in return s1 > s2 })
앞서 본 reversed에 사용된 클로져를 훨씬 간단하게 단축시켰다. 이 형식은 컨텍스트에 따라 타입을 유추하여 동작하는 방식이다. 물론 정확하게 말하자면 유추한다라기 보다는, 애초에 이 클로져가 사용된 sort 함수의 두 번째 인자에는 함수 매개변수 및 리턴 타입에 대해 이미 정의가 되어 있기 때문에 이를 근거로 클로져에 사용되는 매개변수와 리턴 타입을 자동으로 적용하는 것이다. 따라서 유추하는데 모호하면 어떡하냐는 걱정은 필요가 없다.

그런데 여기서 더 단축시키는 방법도 있다. 리턴(return) 자체를 묵시적(implicit)으로 바꿔버리는 것이다. 쉽게 말해서 return 자체도 필요가 없다는 뜻이다.
reversed = sort(names, { s1, s2 in s1 > s2 })
여기서 return을 뺄 수 있는건 논리적 비교가 수행되는 s1 > s2 라는 문장이 Bool 이라는 타입의 데이터를 생성하기 때문이다. 보통 클로져를 sort 에서 쓰는 용도처럼 '클로져 몸뚱아리가 리턴하는 값을 활용'하는 형태로 많이 쓰기 때문에 특정한 타입의 데이터가 최종적으로 명시만 된다면 return을 생략 할 수가 있다.

이제는 약간 다른 클로져 문법을 보자. 이 방식은 별도의 중괄호로 클로져를 구현하는데 구현할 내용이 길어질 때 유용하게 쓸 수 있는 방식이다.
reversed = sort(names) { $0 > $1 }
끝자락에 붙는 클로져(Trailing Closure)라 불리는 형식이다. 이 방식은 약간 제한이 있는데, 함수의 클로져 매개변수가 가장 마지막에 올 때 쓸 수 있는 문법이라는 제한이다. sort() 함수의 경우 클로져를 가장 마지막 매개변수에 쓸 수 있게 정의되어 있기에 가능하다. 단지 매개변수 이름이 $0 부터 시작해서 $1, $2 ... 와 같은 식으로 순서를 메겨서 구현한다는 점이 좀 다르다.

함수를 만들어내는 함수

함수 속의 함수(Nested Function)은 단순히 '함수 안에서 정의하는 함수'로도 볼 수 있다. 하지만 '함수 속에 선언된 함수'는 부모(?) 함수가 실행될 때 만들어지기 때문에 클로져의 또다른 형식이기도 하다.
func factory(str: String) -> () -> String {
    func simpleStringFunc() -> String {
        var myString = str
        return myString
    }
    return simpleStringFunc
}

var strA = factory("A")
var strB = factory("B")
println(strA())        // 콘솔에 A 출력
println(strB())        // 콘솔에 B 출력
위 예제의 factory() 함수는 '인자로 전달된 문자열을 리턴하는 함수'를 리턴한다. 리턴 타입을 잘 보면 () -> String이라는 함수의 형식과 비슷한 타입을 취하고 있는데 이는 함수를 리턴한다는 의미이다. 내부에서는 simpleStringFunc 라는 인자로 전달된 str를 특정 변수에 넣은 후 이를 리턴하는 함수를 생성한다. 그리고 생성한 함수를 리턴한다.

리턴된 함수를 변수(strA, strB)에 할당해서 이 변수를 함수처럼 실행시키는 것도 가능하다. 마치 자바스크립트와 비슷하다.

이번에는 함수 내부의 변수와 함수 속의 함수간의 관계에 대해 알아보자. 아래 예제의 incrementFunc() 는 실행 될 때 마다 0에서 특정 수치를 증가시키는 클로져 함수를 리턴하는 함수다.
func incrementFunc(incValue: Int) -> () -> Int {
    var parentValue = 0
    func nestedFunc() -> Int {
        parentValue += incValue
        return parentValue
    }
    return nestedFunc
}

var inc10 = incrementFunc(10)
inc10()      // 10
inc10()      // 20
inc10()      // 30
inc10()      // 40
inc10()      // 50

var inc20 = incrementFunc(20)
inc20()      // 20
inc20()      // 40
inc20()      // 60
inc20()      // 80
inc20()      // 100
incrementFunc()를 이용해 생성된 클로저 함수를 두 개의 변수 inc10과 inc20에 담았고 이를 별도로 실행시켰다. 실행 결과는 주석으로 표기한 것이다. 그냥 코드를 얼핏 읽어보면 이런 식의 결과가 나올 것 같진 않지만 결론적으로 둘은 따로 동작하지만 결과를 내부에 계속 보관하고 있다. 

동적으로 생성된 함수(Nested Function)나 클로져는 생성 당시 시점의 데이터를 모두 복사해서 사라지기 전 까진 보관한다. incrementFunc의 경우 parentValue가 0으로 초기화 되고 별 다른 사용이 없는데 따라서 incrementFunc 함수가 실행될 때는 항상 parentValue가 0이다. nestedFunc() 는 parentValue를 가지고 내부에서 이 값을 증가시켜서 리턴하는 구조다.

결과적으로 inc10 과 inc20은 같은 함수에서 생성되었지만 서로 다른 메모리를 가지고 있다는 점이다. 클로져는 동적으로 생성되는 함수이고 생성 당시의 주변의 값(?)을 몽땅 복사해서 가지고 시작한다. 클로져가 아닌 일반적인 함수라면 static 같은 정적 변수가 아니고서는 비슷하게 흉내내기 힘든 특징이다.

@autoclosure

설명으로는 암시적인 클로져를 생성한다고 하는데 뭐가 다른지는 잘 모르겠다. 어쨌든 예제를 보자.
func matchWithClosure(closure: @autoclosure () -> Bool, p: Bool) -> Bool {
    return (closure() == p)
}

let v = 10
matchWithClosure(v + v == v * 2, true)    // true
matchWithClosure(v + v == v * 2, false)   // false
오토클로져는 매개변수 타입 앞에 @auto_closure를 붙임으로써 정의가 가능하다. 단 오토클로져는 매개변수를 가질 수 없다는 제약이 있다.

특정 수식을 넘기면 이를 클로져 형식으로 바꾼뒤 넘어가는 형식 같은데 어떻게 써 먹어야 할지를 모르겠다는게 단점같다. -_-;;

어떻게 쓰는지는 @autoclosure 이야기 글을 보자.

참고) @auto_closure 가 @autoclosure 로 이름이 변경되었다.

변수나 상수에 직접 클로져 담기

자바스크립트 처럼 변수에 바로 함수를 할당하는 방식과 비슷하게 클로져를 팩토리 함수가 아닌 방법으로 만들 수 있을까 해서 해 봤는데, 결론적으로 아래와 같은 방식으로는 성공하였다.
let myClosure: (Int, Int) -> Int = {
    return $0 + $1
}
myClosure(1, 2)       // 3
문법이 헷갈릴 수도 있지만, 타입 대신 클로져를 정의하는 스타일을 넣어서 몸뚱아리를 대입시키는 방식이다. 자바스크립트가 연상되어서 참 난감하다. -_-;;

그나저나 이렇게 클로져를 만들 때는 매개변수 이름을 줄 수 없다는 또 다른 단점이 있다. 주의하자.

필요하다면 타입 별칭을 만들어서 클로져 대량 양산(?)의 꿈을 키우는 것도 가능하다.
typealias MyClosureType = (Int, Int) -> Int
let myClosure: MyClosureType = {
    return $0 + $1
}
앞의 예제와 동일하고 단지 타입 정의 부분만 typealias로 분리해 낸 예제이다. 아마도 이런 스타일은 종종 쓰일지도 모르겠다.

[관련글] Swift Memory Management #4 클로져(Closure)의 경우
[관련글] Swift 2.0 - CFunctionPointer 대신 클로저 사용하기​
[관련글] Swift - @noescape 너 정체가 뭐냐
[관련글] Swift - @autoclosure 이야기
[돌아가기] 스위프트(Swift) 가이드

댓글

수악중독님의 메시지…
덕분에 많이 배워 갑니다. 감사합니다.
익명님의 메시지…
좋은 글 감사합니다.

이 블로그의 인기 게시물

소수점 제거 함수 삼총사 ceil(), floor(), round()

버전(Version)을 제대로 이해하기