[Swift] 클로저(Closure)에 대해 알아보자

안녕하세요~ 차니에요!

 

오늘은 Swift의 클로저에 대해 알아보도록 하겠습니다.

 

1. 클로저(Closure)란?

일정 기능을 수행하는 코드의 블럭을 뜻하며 일급 객체의 역할을 할 수 있다.
  • Named Closure : 이름이 있는 클로저로 일반적인 함수가 이에 해당합니다.
  • Unnamed Closure : 이름이 없는 익명 함수로 일반적으로 클로저라 함은 Unnamed Closure를 지칭합니다.

Swift에서 함수는 일급 객체이므로 클로저(익명 함수) 또한 일급 객체로서의 역할이 가능합니다.

 

2. 클로저 표현 방식

{ (Parameters) -> Return Type in
	// Code ...
}

아래와 같이 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태를 Inline Closure 라고 합니다.

let list = [15, 23, 12, 33, 46]

let sorted = list.sorted(by: { (v1: Int, v2: Int) -> Bool in
    return v1 < v2
})

print(sorted) // 12, 15, 23, 33, 46

클로저(익명 함수) 안에 파라미터는 (v1, v2) 리턴 타입은 Bool 형태이며 배열의 값들을 오름차순 정렬하는 기능을 수행하는 클로저입니다.

 

3. 클로저 축약

클로저는 메서드의 특징을 알고 있다면 축약하여 사용할 수 있습니다. (클로저 경량 문법)

클로저 축약은 코드 가독성을 높여주지만 과도한 축약은 오히려 이해하기 난해한 코드가 될 수 있으므로 적당히 사용하는 것이 좋습니다!

 

3-1. 타입 생략

let sorted = list.sorted(by: { (v1, v2) in
    return v1 < v2
})

타입 추론을 통해 list 변수의 자료형(Type)이 Int 임을 알 수 있으므로 파라미터 타입을 생략하여 사용할 수 있습니다.

마찬가지로 반환 타입도 생략 가능합니다.

 

3-2. 반환 키워드 생략

let sorted = list.sorted(by: { (v1, v2) in
    v1 < v2
})

클로저 내부의 코드가 한 줄이라면 return 키워드를 생략하여 표현할 수 있습니다.

 

3-3. 파라미터 이름 생략

let sorted = list.sorted(by: {
    $0 < $1
})

파라미터 이름을 생략하여 표현할 수 있습니다.

코드 블럭 안에서 $0 ... $n 까지 파라미터를 순서대로 호출하여 사용 가능합니다.

 

3-4. 연산자 함수

let sorted = list.sorted(by: <)

연산자를 사용할 수 있는 타입의 경우 연산자만 표현하여 생략이 가능합니다.

 

3-5. 후행 클로저 (Trailing Closure)

let sorted = list.sorted {$0 < $1}

함수의 마지막 인자가 클로저라면, 함수 호출 () 괄호 대신 {} 중괄호를 사용하여 호출이 가능합니다.

이때 argument label(by: )은 생략됩니다.

 

4. 클로저 값 캡쳐(Capturing Values)

func incrementer(v: Int) -> () -> Int {
    var amount = 0
    
    func plus() -> Int {
        amount += v
        return amount
    }
    
    return plus
}

let plusTwo = incrementer(v: 2)
print(plusTwo()) // 2
print(plusTwo()) // 4
print(plusTwo()) // 6

클로저에서 캡쳐란 인스턴스의 프로퍼티에 접근하거나 인스턴스의 메소드를 호출하는 것을 캡쳐라고 합니다.

클로저의 값 캡쳐는 주소 참조입니다. plusTwo 변수는 같은 클로저를 참조하기 때문에 값이 계속해서 증가되는 모습을 볼 수 있습니다.

 

4-1. 클로저의 강한 순환 참조(strong reference cycle)

만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고 그 클로저가 그 인스터스를 캡쳐하면 강한 순환 참조가 발생합니다.

쉽게 말해 서로가 서로를 참조하여 참조 카운트(Refernce Count; RC)가 0이 되지 않아서 메모리가 해제되지 않고 Heap 영역에 계속 남아있는 것을 의미합니다.

class Person {
    var name:String = ""
    
    lazy var getName: () -> String? = {
        return self.name
    }
    
    init(name: String) {
        self.name = name
    }
 
    deinit {
        print("Deinit Person")
    }
}

getName의 클로저에서 self.name의 값을 캡쳐하여 강한 순환 참조가 발생하는 예제 코드입니다.

 

nil을 대입하였지만 강한 참조로 인해 메모리 해제(deinit)가 되지 않습니다.

 

4-2. 강한 순환 참조 해소 방법(weak, unowned)

이를 해소하기 위해 클로저의 선언부에 Capture Lists를 정의하여 약한 순환 참조로 수준을 낮춰주도록 합니다.

lazy var getName: () -> String? = { [weak self] in
	return self?.name
}

약한(weak)참조로 캡쳐 리스트를 정의하였습니다.

lazy var getName: () -> String = { [unowned self] in
	return self.name
}

미소유(unowned)참조로 캡쳐 리스트를 정의하였습니다.

 

weak는 옵셔널 타입이고 unowned는 논옵셔널 타입이라는 차이가 있습니다.

약한 참조로 인해 메모리에서 해제 됩니다.

 

4. 클로저의 종류

4-1. 탈출 클로저 (escaping closure)

함수의 인자로 전달한 클로저가 함수가 끝나고 실행되는 것

일반적인 클로저(non-escaping closure)들은 해당 함수가 끝나기 이전에 실행된다는 점, 해당 함수 내부에서만 쓰일 수 있다는 특징이 있었습니다. 이를 "탈출 불가 상태" 라고 표현합니다. 반대로 "탈출 클로저"는 함수 외부에서 쓰일수 있도록 탈출 시키는 클로저입니다.

주로 비동기 처리가 필요한 네트워크 통신 등에서 많이 쓰입니다.

func printName(_ closure: @escaping (String) -> ()) {
	let name = "channy"

	print("func start")

	DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
		closure(name)
	}

	print("func end")
}

클로저 앞에 @escaping으로 탈출 클로저임을 명시해줍니다.

함수 실행 10초 후 탈출 클로저를 실행하는 코드입니다. 

printName { (name) in
	print(name)
}

실제로 함수가 종료되었지만 escaping 클로저가 실행됨

Alamofire.request(urlRequest).responseJSON { response in

}

대표적으로 네트워크 통신에 쓰이는 Alamofire 라이브러리를 보면 위와 같은 구조입니다.

 

4-2. 자동 클로저 (auto closure)

함수의 인자로 전달되는 코드를 블럭으로 감싸 자동으로 클로저로 만들어주는 것을 의미합니다.
var kakaoFriends = ["muzi", "ryon", "apeach", "neo", "tube", "con"]

func removeAt(_ closure: @autoclosure () -> String) -> String {
    return closure()
}

print(removeAt(kakaoFriends.removeFirst())) // "muzi"

자동 클로저를 적용한 코드입니다. 클로저 앞에 @autoclosure 임을 명시합니다.

{} 중괄호 없이 바로 메서드를 넘겨줄 수 있습니다.

 

func removeAt(_ closure: () -> String) -> String {
    return closure()
}

print(removeAt({
    kakaoFriends.removeFirst() // "muzi"
}))

일반 클로저 코드입니다.

{} 중괄호를 사용하여 파라미터로 클로저를 넘겨주는 모습입니다.

 

이번 포스팅은 쓰다보니 생각보다 길어졌네요 :D

도움이 되셨다면 공감 한 번 부탁드리겠습니다~!