4T2F / ThinkBig2

🌟씽크빅 2팀 스터디 🌟
2 stars 0 forks source link

Swift에서 클로저(Closure)란 무엇이며, 어떻게 사용하나요? #34

Open kmh5038 opened 6 months ago

kmh5038 commented 6 months ago
kmh5038 commented 6 months ago

1. 정의

클로저는 사용자의 코드 안에서 전달되어 사용할 수 있는 로직을 가진 중괄호 구분된 코드의 블럭입니다. 클로저는 두 가지 종류가 있습니다.
이름이 있는 함수(Named Closure), 익명함수(Unnamed Closure)
우리가 보통 부르는 함수는 이름이 있는 함수(Named Closure)로 클로저라 부르지 않지만 사실은 클로저입니다.

func thinkBig() {
    print("Closure")
}

우리가 평소에 부르는 클로저는 익명함수(Unnamed Closure)를 클로저라 부릅니다.

let thinkBig = { print("Closure") }


2. 후행 클로저(trailing closure)

후행 클로저는 클로저 표현을 간소화 하는 방법입니다. 클로저를 좀 더 쓰기 편하게, 보기 편하게 하기 위한 방법입니다. 함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법입니다. 이때, Argument Label은 생략됩니다.

func closureFunc(closure: () -> ()) {
    // 함수 내부에서 클로저 실행
}

이런 코드를

closureFunc {
    // 클로저의 내용을 여기에 작성
}

이렇게 간략하게 표현하여 가독성을 향상 시킬 수 있습니다.


3. 캡처 리스트(Capture List)

캡처 리스트값 타입 일때는 클로저 내부에서 클로저 외부의 값을 참조할 때 참조하는 값이 변경되면 클로저 내부에서도 참조하는 값 또한 바뀌게 되므로 이를 방지하고자 주로 사용되고, 참조 타입 일때는 클로저의 강한 참조 순환 문제를 해결하기 귀해 사용됩니다.


3-1. 값(Value) 타입 캡처 리스트

클로저 내부에서 외부의 값을 참조할 때 의 예시입니다.

var n = 0

var numberPrint = {
    print("반환값: \(n) 입니다.")   // 클로저 내부에서 외부 변수 사용
}

numberPrint()   // 반환값: 0 입니다.

n = 10
numberPrint()   // 반환값: 10 입니다.

n = 100
numberPrint()   // 반환값: 100 입니다.

위 코드처럼 클로저 내부에서 외부 변수를 캡처를 하는 경우인데 변수의 주소값을 힙(Heap) 영역에 저장하기 때문에 값이 변경되면 클로저 내부에도 변경이된다.


캡처 리스트에 의한 캡처의 예시입니다.

var n = 0

var numberPrint = { [n] in  // 캡처 리스트 구현
    print("반환값: \(n) 입니다.")   // 클로저 내부에서 외부 변수 사용
}

numberPrint()   // 반환값: 0 입니다.

n = 10
numberPrint()   // 반환값: 0 입니다.

n = 100
numberPrint()   // 반환값: 0 입니다.

이 경우는 외부 변수 값 자체를 힙(Heap) 영역에 저장하여 외부 변수에 다른 값을 할당하더라도 내부 값이 변경되지않습니다.


3-2. 참조(Reference) 타입의 캡처 리스트

강한 참조가 문제가 되는 예시를 설명하겠습니다.

class Person {
    var name: String
    var run: (()->Void)?

    init(name: String) {
        self.name = name
    }

    func runClosure() {
        run = {
            print("\(self.name)이 달리고 있습니다.")
        }
    }

    deinit {
        print("\(self.name) 메모리에서 제거되었습니다.")
    }
}

func doSomething() {
    var phang: Person? = Person(name: "이창준")  // phang 인스턴스 생성 (phang RC 1증가)
    phang?.runClosure()  // 클로저(run)가 메모리의 Heap 영역에 생성
}

doSomething() 
  1. doSomthing() 함수 작동
  2. phang 인스턴스 생성 (phang RC 1 증가)
  3. phang 인스턴스가 runClosure() 작동
  4. runClosure() 함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성 (클로저(run) RC 1 증가)
  5. runClosure() 함수에서 클로저(run)가 phang을 지목하여 참조하고 있음 (phang RC 1 증가)
  6. doSomething() 함수의 실행이 종료 (phang RC 1감소)

결과 : 최종적으로 phang 인스턴스의 카운트 1, 클로저(run)의 카운트 1 인스턴스와 클로저가 강한 참조 사이클을 유지하고 있기 때문에 소멸자가 작동하고 있지 않습니다.


메모리 누수를 해결한 예시입니다.

class Person {
    var name: String
    var run: (()->Void)?

    init(name: String){
        self.name = name
    }

    func runClosure() {
        run = { [weak self] in
            print("\(self?.name)이 달리고 있습니다.")
        }
    }

    deinit {
        print("\(self.name) 메모리에서 제거되었습니다.")
    }
}

func doSomething() {
    var phang: Person? = Person(name: "이창준")  // phang 인스턴스 생성 (phang RC 1증가)
    phang?.runClosure()  // 클로저(run)가 메모리의 Heap 영역에 생성
}

doSomething()  // 이창준 메모리에서 제거되었습니다.
  1. doSomething() 함수 작동
  2. phang 인스턴스 생성 (phang RC 1 증가)
  3. phang 인스턴스가 runClosure() 함수 작동
  4. runClosure() 함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성 (약한 참조에 의해 클로저(run) RC 0 증가)
  5. runClosure() 함수에서 클로저(run)가 phang 지목하여 참조하고 있음 (phang RC 1 증가)
  6. doSomething() 함수의 실행이 종료, 함수가 종료됨에 따라 phang 인스턴스가 메모리에서 제거되기 때문에 클로저(run) 또한 메모리에서 제거된다. (phang RC 1 감소)

결과 : 최종적으로 phang 인스턴스의 카운트 0, 클로저(run)의 카운트0 이기때문에 소멸자 작동

4. escaping, non-escaping 클로저

4-1. non-escaping 클로저

func thinkBig(completion: () -> ()) {
    completion()
}
thinkBing {
    print("study hard")
 }

이런 코드처럼 우리가 일반적으로 아무런 키워드 없이 파라미터로 받는 클로저를 모두 non-escaping 클로저라 부릅니다.


func thinkBig(completion: () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}

같은 코드를 3초 뒤에 실행하고 싶어 위와 같이 수정을 하면

이런 에러가 발생합니다. 그 이유는 이름 그대로 탈출이 불가능한 클로저이기 때문에 함수의 "흐름"을 탈출 하지 않는 클로저 이기 때문입니다. 한 마디로 함수가 종료되고 나서 클로저가 실행될 수 없고, 함수가 종료되기 전에 클로저가 사용되어야합니다.

class example {
    var property: (() -> ())

    func closureFunc(_ closure : () -> ()) {
        self.property = closure  // error
    }
}

따라서 위와 같이 함수 외부의 변수에 값을 할당하는 이런 경우도 에러가 발생합니다.


4-2. escaping 클로저

non-escaping 클로저에서 함수의 흐름을 벗어날 수 없던 경우에 @escaping 키워드를 붙여 함수를 탈출하여 사용할 수 있습니다.

func sodeul(completion: @escaping () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}

이렇게 사용을하면 이 클로저는 함수의 실행 흐름에서 벗어나도 상관 없이 실행되는 클로저이기 때문에 에러가 발생하지 않습니다.

class example {
    var property : (() -> Void)?

    func closureFunc(_ closure : @escaping () -> Void ) {
        self.property = closure 
    }
}

이 에시 또한 함수 외부의 변수에 할당을 해주고싶을때 @escaping 키워드를 사용하여 사용할 수 있습니다.

escaping 클로저는 이러한 특징 때문에 비동기작업을 시행할 때 주로 사용이 많이됩니다.