이 포스팅은 Swift 시리즈 20 편 중 3 번째 글 입니다.

  • Part 1 - 01: Optional, Any, AnyObject, nil
  • Part 2 - 02: struct, class, enum
  • Part 3 - This Post
  • Part 4 - 04: Property
  • Part 5 - 05: 상속, 생성자, 소멸자
  • Part 6 - 06: 옵셔널 체이닝과 nil 병합
  • Part 7 - 07: 타입 캐스팅
  • Part 8 - 08: assert, guard
  • Part 9 - 09: Protocol
  • Part 10 - 10: Extention
  • Part 11 - 11: 오류처리
  • Part 12 - 12: 고차함수
  • Part 13 - 13: ARC(Automatic Reference Counting)
  • Part 14 - 14: Access control, Access modifier
  • Part 15 - 15: Generics
  • Part 16 - 16: Optional에 대한 깊은 이해
  • Part 17 - 17: Lazy Variables
  • Part 18 - 18: Enumeration
  • Part 19 - 19: Initialization
  • Part 20 - 20: Concurrency
▼ 목록 보기

클로저

  • 코드의 블럭
  • 일급 시민
    • 변수, 상수로 저장이 가능
    • 전달 인자로 전달이 가능
    • 함수에서 return 할 수 있어야 한다.
    • 예를 들어 String 자료형은 이런 것이 가능하겠지
    • 기존에는 함수가 이러한 일급 객체가 아니었지만, 함수형 프로그래밍에서는 이 방식을 채택하여 좀더 자유롭고 유연한 프로그래밍이 가능하게 된다.
  • 함수 : “이름이 있는 클로저”라고 생각할 수 있다.
// 일반 함수
func sumFunction(a: Int, b: Int)->Int {
    return a+b
}
var sumFunctionResult: Int = sumFunction(a: 2, b: 4)
print(sumFunctionResult) // 6
// 클로저
let sum: (Int, Int)-> Int = { (a: Int, b: Int) in
    return a+b
}
let sumClosureResult = sum(1, 4)
print(sumClosureResult)
  • 잘 보게되면 변수에 함수를 할당하는 그림이다.
  • 이것이 가능한 이유는 일급 시민이기 때문이다.
  • 이 때 이 함수의 형태 자체를 자료형, 클래스, 구조체등을 명시하는 것과 같이 선언해준다.
  • 그리고 해당 함수부의 내용은 righthandside에 적어준다.
  • 이 때, 함수의 parameter를 나타낸 뒤, in 이라는 키워드를 적고 내용을 적으면 된다.

함수의 전달인자로서의 클로저

  • 이러한 클로저는 특정 함수를 실행하는데 있어 전달인자로 함수를 줘야할 경우 많이 사용하게 된다.
let add: (Int, Int) -> Int
add = { (a: Int, b: Int) in
    return a + b
}

let subtract: (Int, Int) -> Int
subtract = { (a: Int, b: Int) in
    return a - b
}

let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) in
    return a / b
}

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

print(calculate(a: 50, b: 10, method: add)) // 60
print(calculate(a: 50, b: 10, method: subtract)) // 40
print(calculate(a: 50, b: 10, method: divide)) // 5
  • 또한 이렇게 함수를 클로저로 정의하고 사용하지 않고 바로 작성할 수도 있다.
print(calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int in
    return left*right
})) // 500

클로저 고급

  • 후행 클로저
  • 반환 타입 생략
  • 단축 인자 이름
  • 암시적 반환 표현

이 네가지를 배워볼 것이다. 말이 어려울 뿐 코드로 이해하자.

// 클로저를 매개변수로 갖는 함수
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}
var result: Int // calculate의 값을 받을 변수

후행 클로저

// 후행 클로저
// 클로저가 함수의 마지막 전달인자일 때,
// 마지막 매개변수의 이름을 생략하고 함수의 소괄호 외부에 클로저를 구현할 수 있다.
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
    return left + right
})
print(result) // 20
result = calculate(a: 10, b : 10) { (left: Int, right: Int) -> Int in
    return left + right
}
print(result) // 20

위에 적혀있는 것이 기본적으로 알고 있는 클로저의 사용방법이다. 그런데 클로저가 해당 함수의 마지막 위치에 있을 경우, 조금 변용된 방법으로 사용할 수가 있는데, 소괄호 외부에 중괄호로 연 뒤, 안에 클로저를 구현해서 같은 결과를 얻을 수 있다.

반환 타입 생략

// 반환타입 생략
// calculate라는 함수는 이미 closure 함수가 Int형을 반환한다는 것을 알고 있다.
// 그렇기 때문에 이를 생략할 수 있다. 다만 in 키워드는 생략 불가
result = calculate(a: 10, b: 10, method: {(left: Int, right: Int) -> Int in
    return left + right
})
print(result) // 20

// 후행 클로저에도 적용가능
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
    return left + right
}
print(result) // 20
  • 클로저를 파라미터로 받는 함수의 경우, 이미 어떤 형식의 클로저가 들어올지 명시를 해두었기 때문에 가능한 방법이다.
  • 해당 함수를 콜할 때, 클로저의 반환 타입을 생략하여도 제대로 함수를 적었다면 동작한다.

단축 인자 이름

// 단축 인자이름
// 굳이 closure에 들어오는 매개변수이름이 필요없다면 명시하지 않고 $를 사용해 접근할 수 있다.
// 이 때 in 키워드는 사용할 필요가 없다.
result = calculate(a: 10, b: 10, method: {
    return $0 + $1
})
print(result) // 20

// 후행 클로저와도 가능!
result = calculate(a: 10, b: 10) {
    return $0 + $1
}
print(result) // 20
  • 클로저에서 사용하는 인자 이름이 필요없다면 단축 인자이름을 사용할 수 있다.
  • 클로저의 매개변수의 순서대로 $0, $1, $3 과 같이 사용한다.

암시적 반환 표현

// 암시적 반환 표현
// 클로저를 파라미터로 받는 함수에서 클로저의 반환형이 있다면
// 클로저의 마지막 줄의 결과값은 암시적으로 반환값으로 취급한다.
result = calculate(a: 10, b: 10) {
    $0 + $1
}
print(result) // 20

// 한줄 표현도 가능
result = calculate(a: 10, b: 10) { $0 + $1 }
print(result) // 20
  • 이전 함수에서 클로저의 반환형을 정의했다면, 굳이 반환하지 않아도 클로저의 마지막줄을 반환값으로 취급한다.

클로저에서 값 캡쳐란?

  • 클로저는 내부함수와 내부함수에 영향을 미치는 주변 환경을 모두 포함한 객체이다.
  • 말이 어렵다.
 

func doSomething() {
    var message = "Hi i am sodeul!"
 
    //클로저 범위 시작
    
    var num = 10
    let closure = { print(num) }
 
    //클로저 범위 끝
    
    print(message)
}
  • 이 상황에서 message라는 변수는 클로저 안에서 사용하지 않으니 클로저 안에서 내부적으로 저장하지 않음
  • num은 사용하니, 해당 클로저에서 값을 저장함
  • 이 익명함수는 기본적으로 reference type으로 동작한다고 함
  • 이 때, num이라는 값을 클로저 내부적으로 저장한다는 것을 값이 캡쳐되었다. 라고 함
  • 그럼 값을 어떻게 캡쳐하는데?

값 캡쳐 방식

  • 값을 캡쳐할 때, Value/Reference 타입에 관계없이 Reference Capture 한다.
  • 일반적으로 struct, enum, int와 같은 타입은 값타입이다.
  • 즉 stack 영역에 저장이 된다는 것.
    • 사실 struct 값이 커지면 heap 에 저장되기도 함
  • 근데 클로저는 이 값들을 참조한다는 것
  • 일단 클로저는 heap 영역에 저장될 것이고(참조 타입)
  • 그 안에서 변수의 주소를 들고 있다는 것
  • 이를 Referece Capture라 함
func doSomething() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = {
        print("num check #3 = \(num)")
    }
    
    num = 20
    print("num check #2 = \(num)")
    closure()
}
  • 만약이렇게 있다면, 클로저는 num 이라는 변수의 주소를 들고 있음
  • 이말은, 해당 클로저에서 값을 변경하게되면 해당 변수의 값도 변경된다는 이야기

캡쳐 리스트

  • Value type으로 값을 캡쳐하자.
  • 클로저의 시작인 { 옆에 []을 이용해 캡쳐할 멤버를 나열한다.
let closure = { [num1, num2] in
    // something..
}
  • 값 타입으로 변수를 복사하고 싶은 경우 이렇게 리스트 형식으로 명시적으로 적어주면 된다.
  • 그런데 문제는, 이렇게 Value Type으로 캡쳐한 경우,
    • 선언할 당시의 num 값을 Const Value Type으로 캡쳐한다.
    • 아마 멀티 프로세스 환경에서 병렬 처리 목적으로 이러한 부분을 명시하지 않았나 싶다.
    • 따라서 변경이 불가하다.
    • 추가
      • 상태 값에 따라서 프로그래밍은 문제가 많이 발생한다.
      • 이런 방법을 사용하면 상태 값에 독립적으로 연산이 가능
  • 그럼 Reference Type 값도 Capture List에 작성하면 어떻게 되지?

class Human {
    var name: String = "Sodeul"
}
 
var human1: Human = .init()
 
let closure = { [human1] in
    print(human1.name)
}
 
human1.name = "Unknown"
closure()
  • 이 코드에서 human1은 분명 reference type이다.
  • 이 상태에서 human1 을 캡쳐하면, 복사가 될까?
  • 출력 결과
    • Unknown
  • 안된다. 즉 여전히 Reference type은 Reference Capture한다.
  • 어떻게 하면 원하는 동작을 할 수 있게 할까?

Closure & ARC

  • 클로저와 인스턴스간의 관계
  • 클래스 안에서 클로저를 변수로 갖는 형태를 만들어 보자.
 class Human {
    var name = ""
    // lazy : 사용되기 전까지는 연산이 되지 않음
    lazy var getName: () -> String = {
        return self.name
    }
    
    init(name: String) {
        self.name = name
    }
 
    deinit {
        print("Human Deinit!")
    }
}

 var wansik: Human? = Human(name: "choiwansik")
 print(wansik?.getName)
wansik = nil
  • lazy의 이유
    1. 쟤는 지금 프로퍼티임
    2. 프로퍼티는 인스턴스가 생성될 때 무조건 초기화가 되어있는 상태여야 함
    3. 멤버 메소드는 인스턴스가 초기화가 되어 있다는 가정이 되어 있기 때문에 해당 블록 안에서 self 사용이 가능
    4. 지금 쟤는 프로퍼티로서 할당이 되어 있는데, 초기화 단계에서 self 를 쓴다는 것이 말이 안됨
    5. 그러니까 self를 찾을 수 없다고 뜸
  • 이렇게 만들어 놓고, getName이라는 지연 저장 프로퍼티를 호출해보았다.
  • 그리고 해당 wansik 인스턴스가 필요없어서 nil을 할당했다.
  • 내가 원하는 동작은, 지금 heap 공간에 있는 Human 인스턴스 주소를 wansik이라는 변수가 참조하고 있는데, 이 참조를 끊었으니 reference count가 0이되어 heap 공간에서 할당 해제 되는 것임
  • 하지만… deinit 함수는 호출되지 않는다.

클로저의 강한 순환 참조

  • 클로저는 참조 타입
  • Heap에 상주
  • getName을 호출하면, getNAme 클로저가 Heap에 할당
  • 그리고 그 클로저를 해당 Human 인스턴스의 프로퍼티가 참조
  • 자, 이상황이면, 클래스 인스턴스가 클로저를 참조, 클로저는 인스턴스를 참조
Human instance closure
2
wansik 변수, closure내에서 참조
human instance 변수인 getName이 참조
  • 이런 상황이 만들어짐
  • 그런데..! 이 때 RC를 보면 human instance의 RC가 2임
  • 응? 그럼 결국 closure가 **강한 참조** 를 한다는 것!
  • 와우 클래스 내에서 클로저를 만드는 경우 상당히 조심해야 함.

클로저의 강한 순환 참조 해결

  • 이제 배운 것을 다 섞어야 함
  • weak & unowned 와 캡쳐 리스트를 섞어야 함
  • 앞에서 캡쳐리스트를 써도 값 타입으로 복사가 안된다는 것을 기억할 것
  • 핵심은 간단하다.
    • 클로저에서 인스턴스를 참조할 때, 참조하는 인스턴스의 RC를 안늘려주면 되지.
class Human {
    lazy var getName: () -> String? = { [weak self] in
        return self?.name
    }
}

class Human {
    lazy var getName: () -> String = { [unowned self] in
        return self.name
    }
}
  • 차이점을 보면, weak의 경우 리턴이 옵셔널이다.
  • unowned의 경우 리턴이 옵셔널이 아니다.
  • weak인 경우, 무조건 옵셔널 타입이기 때문에 옵셔널 체이닝을 해주어야 한다.
  • 그리고 그렇기 때문에 리턴 타입도 옵셔널이어야 한다.
  • 그런데 unowned같은 경우에는 일단 Non optional type이다.
    • swift 5.0에서는 Optional Type이 되긴한다고 한다.
    • 하지만 기본은 그렇게 동작하지 않나보다.
  • 의미도 unowned는, 참조하는 인스턴스의 생존 길이가 더 길 때 사용한다고 되어 있기 때문에, 해당 값이 클로저를 실행하고 난후에 있다는 것이 보장된다.
  • 그렇기 때문에 리턴 타입을 옵셔널을 쓰지 않아도 된다!
  • 좀더 의미가 맞게 사용이 가능하고, 옵셔널 바인딩을 안써도되니 코드가 깔끔해진다.

Closure는 여러개이다.

  1. Named Closure
    • 전역 함수
    • 중첩 함수
  2. Unnamed Closure

지금까지 한 것은 익명함수가 전부였다. 이번에는 나머지에 대해 알아보자.

  • 전역 함수
    • func 쓰고 작성하는 함수
    • 일반적으로 사용하는 함수
    • 어떠한 값도 캡쳐하지 않음
  • 중첩 함수
    • 자신을 포함하고 있는 함수의 값을 캡쳐함
func outer() {
    var num: Int = 0
    
    func inner() {
        print(num)
    }
}
  • inner() 함수는 자신을 포함하고 있는 outer 함수의 num이라는 값을 캡쳐한다.
  • 당연히 이렇게 쓰면 reference capture다.
  • 안그러려면 캡쳐 리스트 사용

@escaping

  • 기본적으로 지금까지 봐왔던 closure는 모두 non-escaping closure이다.
  • 즉,
    • 함수 내부에서 직접 실행하기 위해서만 사용된다.
    • 그래서~ 파라미터로 받은 클로저를 변수, 상수에 대입할 수 없다.(실행 목적이니까)
    • 중첩 함수에서 클로저를 사용할 경우 중첩 함수를 리턴할 수 없다.
    • 함수의 실행 흐름을 탈출하지 않아, 종료되기 전에 무조건 실행되어야 한다.
  • 원래는 클로저는 1급 객체이기 때문에, 변수 할당, 리턴, 매개변수로 넘길 수 있어야 했는데 그게 안된다는 것

image@escaping

  • 이런 에러가 뜬다.
  • 또한 함수의 흐름을 탈출하지 않는다는 말은, 함수가 종료된 후에 클로저가 실행될 수 없다는 말이다.

image

  • 이렇게 클로저 함수가 함수 끝나고 실행되기 때문에 에러가 난다.
  • 또, 중첩함수 내부에서 매개 변수로 받은 클로저를 사용할 경우 리턴이 불가하다.

image

왜 나눈걸까?

  • 결국 클로저를 함수 내부에서 실행하지 않을 경우, 클로저가 갖는 특징
    • 리턴
    • 할당
    • 반환
  • 을 사용할 수 없다는 것이다.
  • 왜??
  • 이건 컴파일러의 입장을 들어봐야 한다.
    • 만약 non escaping인 경우, 클로저가 해당 함수 내에서만 사용된다는 것이 보장되기 때문에, 메모리 관리를 지저분하게 하지 않아도 되어 성능이 향상되기 때문이다.
    • non escaping의 경우, 함수가 종료되면 동시에 클로저도 사용이 끝나기 때문에 클로저도 사용이 종료
    • escaping의 경우 함수가 종료되어도 실제 클로저가 사용되지 않을 때까지 메모리를 추적해야 한다.
    • 비동기로 통신 처리할 때, 함수가 끝난 후에 다른 스레드로 넘어가서 해당 클로저를 실행하기 때문에 @escaping 으로 처리해야 한다.