본문 바로가기

문법

[Swift] Closure 알아보기 1부(Feat. 함수타입?)

주의 : 영문서 번역 중 오역이 있을 수 있으며 정확하지않는 정보가 있을 수 있습니다.

 

카테고리
- 클로저
- 기본클로저
- 후행클로저
- 클로저 표현 간소화
- 문맥을 이용한 타입유추
- 단축인자이름
- 암시적 반환 표현
- 연산자 함수
- 값 획득
- 포인트정리
- 궁금증

 

 

클로저란 => 여러곳에 사용될 수 있는 자체 기능블록이 포함된것이다.

 

스위프트에서 함수평 프로그래밍 패러다임을 접할 때 첫걸음으로 꼭 알아야할 녀석이 바로 클로저이다. 클로저를 잘 이해해야 스위프트의

함수형 프로그래밍 패러다임 스타일을 좀 더 명확하게 이해할 수 있다. 클로저와 제네릭, 프로토콜, 모나드등이 결합해서 스위프트는 훨씬 강력한 언어가 되었습니다. 스위프트의 클로저는 C언어나 Objective-C의 블록 또는 다른 프로그래밍 언어 람다와 유사하다 클로저는 일정기능을 하는 코드를 하나의 블록으로 모아놓은 것을 말한다. 뭔가 함수랑 비슷하지않나요 ? 사실 함수는 클로저의 한 형태이다.

 

클로저는 변수나 상수가 선언된 위치에서 참조를 획득하고 저장할 수 있다. 이를 변수나 상수의 클로징(잠금)이라고 하며 클로저는 여기서 착안된 이름이다. 획득 때문에 메모리에 부담이 가지 않을까 걱정할 수 도 있지만, 앞서 설명했듯이 스위프트는 스스로 메모리관리를 합니다.

 

 

클로저의 몇 가지 모양 중 하나가 함수이다. 이미 앞에서 배웠는데 여기서는 함수가 클로저의 몇 가지 모습중 하나의 속한다는 것을 확인해볼 수 있다.

 

- 이름이 있으면서 어떤 값도 획득하지 않은 전역함수의 형태

- 이름이 있으면서 다른 함수내부의 값을 획득할 수 있는 중첩된 함수의 형태.

- 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태

 

애플은 콜러즈의 문법이 정갈하고 깔끔한 스타일이라고 주장하지만, 클로저 문법에 난색을 표하는 부분이 있을것이다. (필자도 난색으로 표하는 사람이다.) 물론 클로저의 문법에 난색을 표했다고해서 클로저 자체의 기능이 별로이거나 불필요하다라는 뜻은 아니다. 오히려 클로저를 빼놓고는 스위프트를 논할 수 없을 만큼 중요한 내용이다.

 

먼저 클로저 문법을 살펴보기전 클로를 얼마나 다양하게 표현할 수 있는지 잠깐 살펴보자

 

- 클로저는 매개변수와 반환 값의 타입을 문맥을 통해 유추할 수 있기때문에 매개변수와 반환값의 타입을 생략할 수 있다.

- 클로저에 단 한줄의 표현만 들어있다면 암시적으로 이를 반환 값으로 취급한다.

- 축약된 전달인자 이름을 사용할 수 있다.

- 후행 클로저 문법을 사용할 수 있다.

 

이제부터 클로저형 문법을 알아보자. 그런데 이렇게 함수를 중첩하여 사용하기보다는 조금 더 간단한 형태로 함수처럼사용하고 싶을 수 있고 또는 함수내부에서 다른 함수를 사용할 때 내부 함수에 이름을 붙일 필요가 없다. 이런 여러가지 경우에 함수 또는 메서드의 전달인자로 함수를 받아오면 된다.

 

클로저 표현방법은 클로저가 함수의 모습이 아닌 하나의 블록의 모습으로 표현될 수 있는 방법을 의미함. 클로저 표현 방법은 클로저의 위치를 기준으로 크게 기본 클로저 표현과 후행 클로저 표현이 있다. 또 각 표현 내에서 가독성을 해치지 않는 선에서 표현을 생략하거나 축약할 수 있는 방법이 있다. 우선 두 클로저 표현 방법(기본 클로저 및 후행 클로저)을 살펴보자 (이후에 축약표현을 알아보자)

 

 

 

# 기본 클로저

스위프트 라이브러리의 sorted(by:) 메서드 정의

public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element]
// String타입의 배열에 이름을 넣어 영문 알파벳으로 내림차순으로 정렬하려고 한다.

 

 

정렬에 사용될 이름 배열

let names: [String] = ["wizplan", "eric", "quokka", "jenny"]

sorted(by:)메서드는 (배열의 타입과 같은 두개의 매개변수를 가지며 Bool타입을 반환하는) 클로저를 전달인자로 받을 수 있다.

반환하는 Bool값을 첫 번째 전달인자 값이 새로 생성되는 배열에서 두번쨰 전달인자 값보다 먼저 배치되어야 하는지에대한 결과값이다.

true를 반환하면 첫번째 전달인자가 두번째 전달인자보다 앞에온다.

 

 

TIP

전달인자로 함수를 보낸다?

함수를 메서드의 전달인자로 보내는일은 함수형 프로그래밍 패러타임에서는 아주 당연한 일입니다. 아직 함수형 패러타임이 익숙하지 않은 독자분은 이번 파트를 읽으면서 천천히 익혀보길 바랍니다. 우선 클로저의 문법을 익힌다는 생각으로 먼저 훑어보고 나중에 차츰차츰 되돌아보면 더욱 수월하게 이해할 수 있을것이다. 함수형 프로그래밍 패러다임에 대해 더 알고싶다면 함수형 사고를 참고해도 좋다.

 

우선은 우리가 기존에 익숙한 방법대로 매개변수로 String타입 두개를 가지며, Bool타입을 반환하는 함수를 구현해보자. 구현된 함수를 sorted(by: ) 메서드의 전달인자로 전달하여 reversed라는 이름의 배열로 반환받는다. 앞 서 설명햇듯 전달받는 두 전달인자는 정렬에 참고할 값이고, 반환된 값은 첫번째 전달인자가 앞으로 배치될지 뒤로 배치될지에 대한 Bool 타입 값이다. 참 함수는 클로저의 한 형태라는 점을 잊지말자

 

 

let names: [String] = ["wizplan", "eric", "quokka", "jenny"]
// 정렬을 위한 함수전달
func backward(first: String, second: String) -> Bool {
    print("\(first) \(second) 비교중")
    return first > second
}
let reversed: [String] = names.sorted(by: backward)
//names 의 요소들을 backward 함수타입형태로 정렬하겠다. 라는의미일까?
print(reversed)// ["wizplan", "quokka", "jenny", "eric"]

 

만약(first) 문자열이 second문자열보다 크다면 backwards(first: second:) 함수의 반환 값을 true가 될것이다. 즉, 값이 더큰 first문자열이 second문자열 보다 앞쪽에서 정렬되어야 한다는 뜻이다. 그러나 first > second라는 반환값을 받기 위해 너무많은 표현을 사용했다. 예시 코드에서 print()함수는 참고용 콘솔출력이니 제외해도 역시 많다. 함수 이름부터 매개변수 표현까지 부가적인 표현도 많다. 이를 클로저 표현을 사용해서 조금더 간결하게 표현하겠다.

 

 

{ (매개변수들) -> 반환 타입 in
     //실행코드
}

클로저도 함수와 마찬가지로 입출력 매개변수를 사용할 수 있다. 매개변수 이름을 지정한 다면 가변 매개변수 또한 사용가능하다. 다만 클로저는 매개변수 기본값을 사용할 수 없다. 이제 backwards(first: second:) 함수를 틀로저 표현으로 대체해보겠다.

 

sorted(by:)메서드에 클로저 전달

//backward(first: second:)함수 대신에 sorted(by:)메서드의 전달인자로 클로즈를 직접 전달한다.
let reversed2: [String] = names.sorted(by: { (first: String, second: String) -> Bool in
    return first > second
})
print(reversed2) // ["wizplan", "quokka", "jenny", "eric"]

sorted(by:)메서드로 전달하는 클로저의 매개변수 개수와 타입, 그리고 반환타입이 모두 backwards(first: second:) 함수와 같다는 것을 눈치챘나요? 아직 익숙하지 않겠지만 처음보다 코드가 훨씬 간결해지고 직관적을 바뀌었따.

 

이렇게 프로그래밍 하면 sorted(by:) 메서드로 전달되는 backward(first: second:)함수가 어디에 있는지, 어떻게 구현되어 있는지 찾아다니지 않아도 된다. 물론, 반복해서 같은 기능을 사용하려면 함수로 구현해두는 것도 나쁘지 않는다. 이는 선택하기 나름이다.

=> 견해) 초보자입장에서는 이해하기위해서 직접 명시를 해주는게 좋은것같도 자주사용하고 또 이코드를 읽는사람도 익숙하다면 생략해서 사용해도 좋은것같다.

 

# 후행클로저 

더 클로저를 읽기 쉽게 바꿔볼 수 있다. 함수나 메서드의 마지막 전달인자로 위치하는 클로저는 함수나 메서드의 소괄호를 닫은 후 작성해도 된다. 클로저가 조금 길어지거나 가독성이 조금 떨어진다 싶으면 후행 클로저 기능을 사용하면 좋다. Xcode에서 자동완성 기능을 사용하면 자동으로 후행 클로저로 유도한다.

 

단, 후행클로저는 맨 마지막 전달인자로 전달되는 클로저에만 해당되므로 전달인자로 클로저 여러개를 전달할 때는 맨마지막 클로저만 후행클로저로 사용할 수 있다. 또한 sorted(by:)메서드처럼 단 하나의 클로저만 전달인자로 전달하는 경우에는 소괄호를 생략할 수 있다.

 

또 매개변수에 클로저가 여러개 있는 경우, 다중 후행 클로저 문법을 사용할 수 있다. 다중 후행 클로저를 사용하는 경우, 다중 후행클로저 문법을 사용할 수 있다. 다중 후행 클로저를 사용하는 경우 중괄호를 열고 닫음으로써 클로저를 표현하며, 첫번째 클로저의 전달인자 레이블은 생략한다.

 

// 후행 클로저 표현
let reversed3: [String] = names.sorted() { (first: String, second: String) -> Bool in
    return first > second
}

// sorted(by:)메서드의 소괄호 까지 생략가능하다
let reversed4: [String] = names.sorted { (first: String, second: String) -> Bool in
    return first > second
}

func doSomething(do: (String) -> Void,
                 onSuccess: (Any) -> Void,
                 onFailure: (Error) -> Void) {
    //do Something...
}

// 다중 후행 클로저의 사용
doSomething { (someString: String) in
    // do closure
} onSuccess: { (result: Any) in
    // success closure
} onFailure: { (error: Error) in
    // failure closure
}

Xcode에서도 자동완성으로 후행 클로저를 사용하도록 유도하므로 자주 볼것이다. 또한, 앞으로 배워볼 맵과 필터 등에서도 계속해서 볼 수 있으니 잘 상기하도록 하자.

 

 

# 클로저 표현 간소화

이번에는 클로저 표현을 간소화하는 몇가지 방법에 대해 알아보도록하자.

 

문맥을 이요한 타입유추

메서드의 전달인자로 전달하는 클로저는 메서드에서 요구하는 형태로 전달해야한다. 즉, 매개변수의 타입이나 개수, 반환 타입등이 같아야

전달인자로서 전달할 수 있다. 이를 다르게 말하면 전달인자로 전달할 클로저는 이미 적합한 타입을 준수하고 있다고 유추할 수 있다.

문맥에 따라 적절히 타입을 유추할 수 있는것임. 그래서 전달인자로 전달하는 클로저를 구현할 때는 매개변수의 타입이나 반환 값의 타입을

굳이 표현해주지 않고 생략하더라도 문제가 없습니다.

 

생략된 클로저를 확인해보자

 

클로저의 타입 유추

// 클로저의 매개변수 타입과 반환 타입을 생략하여 표현할 수 있다.
let reversed5: [String] = names.sorted { (first, second) in
    return first > second
}

 

단축 인자 이름

계속해서 sorted(by:) 메서드로 전달하는 클로저를 살펴보면 또 하나 마음에 들지 않는 점이 있다. 바로 의미없어 보이는 두 매개변수 이름이다. first, second라니 정말 아무 의미도 멋도 없다. 그래서 스위프트는 조금 멋스럽고 간결하게 표현할 수 있도록 단축인자이름을 제공한다.

 

단축인자 이름은 첫번쨰 전달인자부터 $0, $1, $2, $3 ...  순서로 $와 숫자의 조합으로 표현한다. 단축 인자표현을 사용하게되면 매개변수 및 반환 타입과 실행 코드를 구분하기 위해 사용했던 키워드 in을 사용할 필요도 없어진다.

단축인자 이름 사용

// 단축 인자 이름을 사용한 표현
let reversed6: [String] = names.sorted {
    return $0 > $1
}

 

 

암시적 반환  표현

점점 더 간결해지고 있는 클로저 표현을 보고 있는데 '이제 더 줄일게 없어!!' 이제 이정도가 한계야 라고 생각을 하고있다. 근데 아직 끝이아니다. 생략할 수 있는 것이 딱하나가 더 있는데 클로저에서는 return 키워드마저 생략할 수 있다.

 

만약 클로저가 반환값을 갖는 클로저이고 클로저 내부의 실행문이 단 한줄이라면, 암시적으로 그 실행문을 반환값으로 사용할 수 있다.

암시적 반환 표현의 사용

// 암시적 반환 표현의 사용
let reversed7: [String] = names.sorted { $0 > $1 }

 

# 연산자 함수

example

클로저의 장점은 간단한 표현이다. 처음소개했던 함수의 표현에서 얼마나 더 간단해졌는지 처음으로 돌아가보면 느낄 수 있다.

 

우리는 연산자에 대해서 알아보았다. 비교연산자는 두개의 피연산자를 통해 Bool타입의 반환을 준다. 우리가 sorted(by:) 메서드에 전달한 클로저와 동일한 조건이다. 클로저는 매개변수의 타입과 반환 타입이 연산자를 구현할 함수의 모양과 동일하다면 연산자만 표기하더라도 알아서 연산하고 반환한다. 이유를 설명할 때 연산자가 일종의 함수였다는 것을 기억하나요 ? 스위프트 라이브러리에서 우리가 사용하는 비교 연산자의 정의를 보자면 다음과 같다.

// > 연산자의 정의
public func ><T: Comparable>(lhs: T, rhs: T) -> Bool

func 키워드가 보이는데 함수는 클로저의 일종! 기억하죠 ? 여기서 > 자체가 함수의 이름이다. 더 군다나 이 함수는 우리가 전달인자로 보내기에 충분한 조건을 갖고 있다. (물론 아직 제네릭과 프로토콜을 배우지 않아 다 이해할 수 없지만 T를 String 으로만 바꿔서 생각해보자)

//연산자 함수를 클로저의 역할로 사용
let reversed8: [String] = names.sorted(by: >)
// >이 연산자가 설계된 타입이 함수타입이었기에....들어갈수있었다니 충격이다.

 

# 값 획득

클로저 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수 획득(Capture) 할 수 있다. 값 획득을 통해 클로저는 주변에 정의한 상수나 변수가 더이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정할 수 있다. 이 이야기를 하는 이유는 클로저가 비동기 작업에 많이 사용되기 때문이다. 클로저를 통해 비동기 콜백(Call-back)을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면, 실제로 클로저의 기능을 실행하는 순간에는 주변의 상수나 변수가 이미 메모리에 존재하지 않는 경우가 발생한다.

 

중첩 함수도 하나의 클로저 형태라고 앞에서 설명했는데, 이 중첩함수 주변의 변수나 상수를 획득해 놓을 수도 있다. 즉, 자신을 포함하는 함수의 지역변수나 지역상수를 획득할 수 있다. 지금부터 incrementer()라는 함수를 중첩함수로 포함하는 makeIncrementer 함수를 살펴보겠다. 중첩 함수인 incrementer() 함수는 자신 주변에 있는 runningTotal 과 amount라는 두값을 획득합니다. 두 값을 획득한 후에 incrementer()는 클로저로서 makeIncrementer함수에 의해 반환된다.

 

makeIncrementer(forIncrement:) 함수

// makeIncrementer(forIncrement:) 함수
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer 함수의 반환 타입은 () -> Int입니다. 이는 함수객체를 반환한다는 의미이다. 반환하는 함수는 매개변수를 받지 않고 반환 타입은 Int인 함수로 호출할 때 마다 Int타입의 값을 반환해준다. incrementer가 반환하게 될 값을 저장하는 용도로 runningTotal을 정의했고, 0으로 초기화해두었다. 그리고 forIncrement라는 전달인자 레이블과 amount라는 매개변수 이름이 있는 Int타입 매개변수가 하나가 있다. incrementer() 함수가 호출될 때마다 amount의 값만큼 runningTotal 값이 증가한다.

또한 값을 증가시키는 역할을 하는 incrementer라는 이름의 중첩함수를 정의했다. 이 incrementer() 함수는 amount의 값을runningTotal에 더하여 결괏값을 반환한다.

 

incrementer()함수를 makeIncrementer(forIncrement: )함수 외부에 독립적으로 떨어뜨려 놓으면 동작할 수 없는 이상한 형태가 된다.

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

incrementer() 함수는 어떤 매개변수도 갖지 않으며, runningTotal이라는 변수가 어디 있는지 찾아볼 수도 없습니다. 지금 이 형태만으로는 잘못된 코드이다.

 

그러나 앞쪽의 incrementer()함수 주변에 runningTotal과 amount변수가 있다면 incrementer()함수는 두 변수의 참조를 획득할 수 있다. 참조를 획득하면 runningTotal과 amount는 makeIncrementer함수의 실행이 끝나도 사라지지 않는다. 게다가 incrementer가 호출될 때마다 계속해서 사용할 수 있다.

 

makeIncrementer(forIncrement:)함수를 사용하여 incrementByTwo라는 이름의 상수에 increment함수를 할당해줬다. incrementByTwo를 호출할 때마다 runningTotal은 값이 2씩 증가한다.

 

incrementByTwo 상수에 할당

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)

let first: Int = incrementByTwo()
let second: Int = incrementByTwo()
let third: Int = incrementByTwo()

print(first) //2
print(second) //4
print(third) //5

아래에 incrementer를 하나 더 생성해주면, incrementerByTwo와는 별개의 다른 참조를 갖는 runningTotal변수값을 확인할 수 있다. 이를 코드로 구현해보자

 

 

각각의 incrementer의 동작

let incrementByTwo2: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTwo3: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTen: (() -> Int) = makeIncrementer(forIncrement: 10)
print(incrementByTwo2) //2
print(incrementByTwo3) //2
print(incrementByTen) //10

let first2: Int = incrementByTwo2()
let second2: Int = incrementByTwo2()
let third2: Int = incrementByTwo2()
print(first2)//2
print(second2)//4
print(third2)//6

let first3: Int = incrementByTwo3()
let second3: Int = incrementByTwo3()
let third3: Int = incrementByTwo3()
print(first3)//2
print(second3)//4
print(third3)//6

let ten: Int = incrementByTen()
let twenty: Int = incrementByTen()
let thirty: Int = incrementByTen()
print(ten)//10
print(twenty)//20
print(thirty)//30

각각의 incrementer 함수는 언제호출이 되더라도 자신만의 runningTotal 변수를 갖고 카운트하게 됩니다.

다른 함수의 영향도 전혀 받지 않습니다. 각각 자신만의 runningTotal의 참조를 미리 획득했기때문이다.

 

NOTE

클래스 인스턴스 프로퍼티로서의 클로저

클래스 인스턴스의 프로퍼티로 클로저를 할당한다면 클로저는 해당 인스턴스 또는 인스턴스의 멤버 참조를 획득할 수 있으나 클로저와 인스턴스 사이에 강한 참조 순환 문제가 발생할 수 있다. 강한참조 순환 문제는 획득목록을 통해 없앨 수 있다. 더 자세한 사항은 ARC의 강한 참조에서 알아보자

 

포인트정리

- 함수는 클로저의 한 형태이다. (() -> Void)

- 클로저는 변수나 상수가 선언된 위치에서 참조를 획득하고 저장할 수 있다.

- 이름있고 값이 없는 전역함수의 형태, 이름있고 값을 가질 수 있는 중첩된 함수형태, 이름없고 주변문맥에따라 축약문법으로 작성한 형태가 있다.

- 매개변수와 반환값을 생략할 수 있다.

- 클로저에 단 한줄의 표현만 들어있다며 암시적으로 이를 반환값으로 취급한다.() = { $0 > $1}

- 축약된 전달인자이름(String, String)을 사용할 수 있다.

- 후행 클로저({}) 문법을사용할 수 있다.

- 매개변수의 타입이나 개수, 반환 타입등이 같아야 전달인자로서 전달할 수 있다.

- 단축 인자표현($)을 사용하게되면 매개변수 및 반환 타입과 실행 코드를 구분하기 위해 사용했던 키워드 in을 사용할 필요도 없어진다.

- 만약 클로저가 반환값을 갖는 클로저이고 클로저 내부의 실행문이 단 한줄이라면 반환값 생략가능함

- 값 획득을 통해 클로저는 주변에 정의한 상수나 변수가 더이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정할 수 있다.

 

궁금증

- 매개변수에 함수타입으로 설정이 되어있으면 클로저를 이용해서 사용할 수 있는것인가?

- 값 획득에서 말하길 클로저내에서 상수나 변수를 생성하여 값을 내부에서 참조할 수 있다고하는데 class참조와 같은 의미인가 ?

- 함수객체를 반환한다라고 말하고있는데 공식문서에보면 객체보단 인스턴스라고 부르는게 더 정확한 표현이다 라는걸 확인했는데

  객체라고 표현한 이유가 있는걸까 ? 아직 인스턴스 정의에대해서 배우지않고 차후 챕터에서 배우기떄문에 이렇게 표현한건가 ?

- 완젼 충격이다 .함수는 stack구조로 되어있어서 실행이 끝나면 변수가 초기화 되야할 텐데 incrementByTwo상수에 담긴 makeIncrementer함수타입의 값이 누적이되는것이다..충격 일반함수실행과는 다른구조인가? 아니면 내가 stack구조라고 잘못알고있었던걸까 ?

 

Reference

- 야곰책 야곰 Swift프로그래밍 3판

- https://docs.swift.org/swift-book/LanguageGuide/Closures.html