새벽의 기록

[SOPT] 문법 스터디 #2 클로저 본문

SOPT/문법 스터디

[SOPT] 문법 스터디 #2 클로저

OneTen 2024. 11. 12. 06:50
클로저
- 클로저의 메모리 관리 방식
- 클로저의 캡처 리스트
- Trailing Closure이 사용되는 예시를 찾아보거나 실제로 구현해보고, 어떤 점이 좋은지 알아봅시다 !
- Escaping 클로저와 Non-escaping 클로저의 차이

 

1. 클로저의 메모리 관리 방식

클로저는 참조타입이라 ARC(Automatic Reference Counting)로 메모리를 관리한다.
ARC(Automatic Reference Counting)는 Swift의 메모리 관리 시스템으로, 클래스 인스턴스의 수명을 관리한다.
→ 각 클래스 인스턴스에 대해 몇 개의 참조가 있는지 추적하여, 참조 카운트가 0이 되면 해당 인스턴스의 메모리를 해제한다.
→ 즉, 참조 카운트가 1 이상이라면 메모리에서 할당 해제되지 않는다.
 

2. 클로저의 캡처 리스트

캡쳐

func doSomething() {
    var message = "Hi i am Kim"
 
    //클로저 범위 시작
    
    var num = 10
    let closure = { print(num) }
 
    //클로저 범위 끝
    
    print(message)
}

익명함수는 closure는 클로저 내부에서 외부 변수인 num이라는 변수를 사용(print)하기 때문에 
num의 값을 클로저 내부적으로 저장하고 있는데, 이것을 클로저에 의해 num의 값이 캡쳐 되었다 라고 표현함.
 
 

클로저의 값 캡쳐 방식

 
Closure는 값을 캡쳐할 때 Value/Reference 타입에 관계 없이 Reference Capture 한다.
→ 즉, 구조체나 배열처럼 값을 복사해서 사용하는 값 타입이든, 클래스처럼 값을 참조해서 사용하는 참조 타입이든 상관없이 Reference 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()
}

// print : num check #1 = 0
// print : num check #2 = 20
// print : num check #3 = 20

closure는 num이라는 외부 변수를 클로저 내부에서 사용하기 때문에 num을 캡쳐한다.
이 때 캡쳐의 방식이 Reference Capture 즉, num이란 변수를 참조!
→ 따라서, closure를 실행하기 전에 num이란 값을 외부에서 변경하면 클로저 내부에서 사용하는 num의 값 또한 변경된다.
 

func doSomething() {
    var num: Int = 0
    print("num check #1 = \\(num)")
    
    let closure = {
        num = 20
        print("num check #3 = \\(num)")
    }
    
    closure()
    print("num check #2 = \\(num)")
}

// print : num check #1 = 0
// print : num check #3 = 20
// print : num check #2 = 20

혹은, 클로저 내부에서 num의 값을 바꾸면 클로저 외부에 있는 num의 값도 변경된다.
 
그럼 만약 Value Type으로 Capture를 하고 싶으면 어떻게 할까??
→ 이 때 사용하는 게 캡쳐 리스트
 
 

클로저의 캡쳐 리스트

let closure = { [num, num2] in

이런식으로 클로저의 시작인 { 바로 옆에 []를 이용해서 캠쳐할 멤버를 나열
→ 이 때 in 키워드도 꼭 작성해주기
 
 
Value Type의 값을 복사해서 Value Capture 하기

func doSomething() {
    var num: Int = 0
    print("num check #1 = \\(num)")
    
    let closure = { [num] in
        print("num check #3 = \\(num)")
    }
    
    num = 20
    print("num check #2 = \\(num)")
    closure()
}

// print : num check #1 = 0
// print : num check #2 = 20
// print : num check #3 = 0

closure를 실행하기 전에 외부 변수 num의 값을 20으로 변경했지만, 클로저의 num에는 영향을 주지 않는다.
 
 
한 가지 유의할 점은,
이 방식은 Closure를 선언할 당시의 num의 값을 Const Value Type으로 캡쳐한다.
→ 즉 상수로 캡쳐한다.
따라서 closure 내부에서 Value Capture된 값을 변경할 수 없음
 
 
그럼 Reference Type의 값을 Value Capture 할 수도 있나?
→ 못한다. Reference Type은 자동으로 Reference Capture함
 
 
그렇다면 Reference Type은 클로저 캡쳐 리스트가 필요 없나?
 
캡쳐 리스트를 사용하면 클로저와 클로저가 캡쳐한 객체 사이의 참조를 약하게(weak) 또는 비소유(unowned)로 설정해 순환 참조를 방지할 수 있다.
→ 클로저의 강한 순환 참조 해결방법!!

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

이런식으로 캡쳐 리스트 앞에 weak 혹은 unowned 작성해서 사용

 

3. Trailing Closure이 사용되는 예시를 찾아보거나 실제로 구현해보고, 어떤 점이 좋은지 알아봅시다 !

트레일링 클로저는 함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법
→ 마지막 파라미터가 클로저면 Argument Label은 생략
→ 파라미터가 하나인 경우 ()도 생략
 
예를 들어,

func doSomething(closure: () -> ()) {
    closure()
}

이것처럼 클로저를 파라미터로 받는 함수가 있을 때, 이 함수를 호출하려고 하면

doSomething(closure: { () -> () in
    print("Hello!")
})

이런 식으로 작성해야했다. 근데 이럴 경우 가독성이 별로 좋지 않아서 트레일링 클로저를 사용!

doSomething () { () -> () in
    print("Hello!")
}

이런 식으로 “closure:” 부분을 생략하고 바로 붙여 쓸 수 있다.
하지만 여전히 ‘() -> () in’ 요런 게 너무 지저분해서 사용하는 게 경량 문법!
 

클로저의 경량 문법

func doSomething(closure: (Int, Int, Int) -> Int) {
    closure(1, 2, 3)
}

이런 함수가 있으면 원래는

doSomething(closure: { (a: Int, b: Int, c: Int) -> Int in
    return a + b + c
})

이런 식으로 작성했었는데, 여기서 경량 문법은 파라미터 형식(‘Int’)과 리턴 형식(‘→’)을 생략할 수 있다.

doSomething(closure: { (a, b, c) in
    return a + b + c
})

여기서 한 번 더, Parameter Name은 Shortand Argument Names으로 대체하고, 이 경우 Parameter Name과 in 키워드를 삭제할 수 있다.
→ ‘a, b, c’ 얘네 대신 $0, $1, $2 얘네들 사용 가능

doSomething(closure: {  
    return $0 + $1 + $2
})

그러면 이렇게 간단화 할 수 있고, 단일 리턴문만 남을 경우 return도 생략할 수 있다.

doSomething(closure: {  
     $0 + $1 + $2
})

그럼 이제 맨 처음 찾아본 트레일링 클로저를 적용할 수 있는데, 클로저 파라미터가 마지막 파라미터면, 트레일링 클로저로 작성할 수 있다.

doSomething() {  
     $0 + $1 + $2
}

파라미터가 하나인 경우 ()도 생략 가능하니 결과물은

doSomething {  
     $0 + $1 + $2
}

이렇게까지 줄여진다. 코드의 길이도 줄고 가독성도 좋아져서 굳.
 

4. Escaping 클로저와 Non-escaping 클로저의 차이

Non-escaping 클로저는 함수 내에서만 실행되며, 함수가 종료되면 메모리에서 해제된다.
→ 기본적으로 클로저는 non-escaping
 
Escaping 클로저는 함수가 종료된 후에도 실행될 수 있으며, 함수 외부로 클로저가 전달될 수 있다.
→ 클로저가 함수의 외부에서 사용되거나 비동기 작업에 전달될 때 사용된다.
→ 함수 외부에서 클로저가 사용될 수 있기 때문에, 클로저가 참조를 강하게 유지하면 순환 참조 문제가 발생할 수 있다.

Comments