Swift

Swift - 클로저의 캡처 파해치기 (값 타입, 참조 타입의 동작 차이)

Goniii 2024. 12. 11. 16:04

클로저(Closure)

  • 코드에서 사용할 수 있는 독립적인 코드블록으로 함수와 동일하게 사용하나 이름이 없는 익명함수로 사용 가능함
  • 참조 타입으로, 힙에 저장되며 ARC가 메모리를 관리함
  • 캡처 기능을 사용
  • 일급 객체로 취급
    • 함수의 참조를 저장하거나 변수에 할당 가능
    • 다른 함수에 매개변수로 전달 가능
    • 함수에서 반환 가능

 

클로저 기본 구문

{ (매개변수 목록) -> 반환 타입 in
    실행 코드
}
  • 매개 변수 목록 : 클로저가 사용하는 입력 값
  • 반환 타입 : 실행 결과로 반환되는 값의 타입
  • in : 클로저 헤더(매개변수 목록, 반환 타입)와 본문을 구문

예제

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

let result = multiplyClosure(4, 5) // 20

 

 

클로저의 캡처(Capture)

  • 클로저는 외부 변수나 상수를 캡처하여 저장 및 참조 가능
  • 값 타입은 복사 되고, 참조 타입은 참조를 유지함

 

캡처의 동작 원리

  1. 클로저는 자신이 정의된 환경(Context)에서 변수나 상수에 접근할 수 있음
  2. 클로저가 실행되는 시점에 관계없이, 외부 변수는 클로저 내부에서 지속적으로 참조되거나 저장됨
  3. 캡처 동작은 값 타입과 참조 타입에 따라 다르게 작동함
    • 값 타입(Struct, Enum) : 캡처 시점에 복사되어 클로저 내부에 저장
    • 참조 타입(Class) : 클로저 내부에서 해당 객체에 대한 강한 참조를 유지

 

래퍼런스 캡처링(Reference Capturing)

  • 클로저가 외부 변수나 상수를 참조 형태로 캡처하는 방식
  • 외부 변수의 변경 사항이 클로저 내에서도 반영됨
  • 클로저 실행 시점의 변수 값을 사용함

특징

  1. 클로저는 변수의 메모리 위치를 참조
  2. 외부 변수의 값이 변경되면 클로저 내부에서도 변경된 값을 볼 수 있음
var a = 0
let closure = {
    print(a) // 래퍼런스 캡처링
}
a = 5
closure() // 5

→ 클로저가 실행될 때 변수 a의 현재 값을 참조함

 

 

 

벨류 캡처링 (Value Capturing)

  • 클로저가 외부 변수나 상수의 값을 복사하여 캡처하는 방식
  • 클로저 실행 시점과 무관하게, 캡처된 값은 변경되지 않음

특징

  1. 캡처 시점의 값을 복사하여 클로저 내부에 저장됨
  2. 외부 변수의 값이 이후에 변경되어도 클로저는 캡처된 초기 값만 사용
var a = 0
let closure = { [a] in // 벨류 캡처링, [a] : 캡처 리스트
    print(a)
}
a = 5
closure() // 0
  • [a] 를 통해 캡처 리스트를 지정하여 a의 값을 복사
  • a의 값을 변경했지만, 클로저 내부에는 복사되었던 값 0을 출력함

 

 

래퍼런스 캡처링과 벨류 캡처링의 차이

특성 래퍼런스 캡처링(Reference Capturing) 벨류 캡처링 (Value Capturing)
캡처 형태 변수의 참조(주소)를 캡처 변수의 값을 복사하여 캡처
변경 반영 여부 클로저 실행 시 외부 변수의 변경 사항 반영 클로저 실행 시 캡처 시점 값 유지
사용 예시 클로저가 외부 변수의 실시간 값을 참조해야 할 때 값이 고정된 상태로 사용해야 할 때

 

 

캡처 리스트 (Capture List)

  • 벨류 캡처링을 명시적으로 지정하려면 캡처 리스트를 사용함
  • 캡처 리스트를 사용하면 특정 변수만 값을 복사하거나 참조 방식을 조정할 수 있음
var a = 0
var b = 0

let closure = { [a, b] in // 캡처 리스트로 벨류 캡처링
    print(a, b)
}

a = 5
b = 10
closure() // 0, 0
  • [a, b]를 캡처 리스트로 지정하여 a, b의 값을 복사함
  • 이후 a, b 값을 변경해도 클로저 내부에서는 캡처 당시의 값 0, 0을 사용함

 

아래와 같은 클로저를 사용하면 어떤 값이 출력될까? 0? 아님 10?

class Test {
    var value = 0
    func setValue(value : Int){
        self.value = value
    }
}

let test = Test()
let closure = { [test] in // 캡처 리스트
    print(test.value)
}

test.setValue(value: 10)
closure()
  • 클로저를 통해 test 객체를 캡처했으니 해당 시점의 value 값 0이 출력될까?
  • 하지만, 출력되는 값은 10이다. 그 이유는 아래 class와 struct에서의 클로저 동작 차이에서 알아보기로 하자

 

Class와 Struct에서 클로저 동작 차이

  • 클로저는 외부 변수나 객체를 캡처할 때 참조 타입과 값 타입에 따라 다르게 동작함
  • class와 struct는 메모리 관리와 캡처 방식에서 중요한 차이를 가지므로 클로저 동작 방식에도 영향을 받음

 

클래스에서 클로저 동작

  • 클래스는 참조 타입
  • 클로저가 인스턴스를 캡처하면 해당 객체에 대한 참조를 캡처함 → 객체 주소 캡처를 의미
  • 클로저 실행 시점에 객체의 현재 상태를 참조함
class Test {
    var value = 0
    func setValue(value: Int) {
        self.value = value
    }
}

let test = Test() // 클래스 인스턴스 생성
let closure = { [test] in // 참조 캡처 (test의 참조를 캡처)
    print(test.value) // 캡처된 test 인스턴스의 현재 value를 참조
}

test.setValue(value: 10) // test 인스턴스의 value를 변경
closure() // 10
  • [test] 는 참조 캡처를 의미함 (주소 복사) → 클래스는 참조 타입이기 때문에 실행 시점의 value 값 10이 출력됨

 

 

구조체에서 클로저 동작

  • 구조체는 값 타입
  • 클로저가 구조체를 캡처하면 해당 시점의 값을 복사함
  • 클로저 실행 시점에 구조체의 변경된 값은 반영되지 않음
struct Test {
    var value = 0
    mutating func setValue(value: Int) {
        self.value = value
    }
}

var test = Test() // 구조체 인스턴스 생성
let closure = { [test] in // 값 캡처 (test 자체 값을 캡처)
    print(test.value) // 캡처된 test의 복사본 값
}

test.setValue(value: 10) // test 인스턴스의 value 변경
closure() // 0
  • [test]는 값 캡처를 의미함(값 복사) → 구조체는 값 타입이기 때문에 캡처 시점의 value 값 0 출력

 

 

그럼 아래 코드에서는 어떤 이름이 출력될까?

struct Friend {
    var name: String
    init(name: String) {
        self.name = name
    }

    func printName() {
        print("Name is \(self.name)")
    }
}

var friend = Friend(name: "민수")
friend.name = "철수"

let closure = friend.printName

friend.name = "영희" 
closure()

 

정답은 “Name is 철수”이다

  1. 구조체는 값 타입(Value Type)
    • friend가 printName 메서드를 캡처할 때, 현재 friend의 복사본을 캡처함
    • 따라서, 이후 friend.name을 "영희"로 변경해도 클로저 내에서 캡처된 값은 변경되지 않음
  2. 클로저에서 캡처된 값
    • let closure = friend.printName는 friend의 복사본에 있는 printName 메서드를 캡처
    • 실행 시점에서 캡처된 복사본의 name 값인 "철수"를 출력

 

클래스에서의 동작

class Friend {
    var name: String
    init(name: String) {
        self.name = name
    }

    func printName() {
        print("Name is \\(self.name)")
    }
}

var friend = Friend(name: "민수")
friend.name = "철수"

let closure = friend.printName // 클로저에 printName 메서드 캡처

friend.name = "영희" // friend의 name 변경
closure() // "Name is 영희" 출력
  1. 클래스는 참조 타입(Reference Type)
    • friend가 printName 메서드를 캡처할 때, friend 객체의 참조 주소를 캡처
    • 이후 friend.name을 "영희"로 변경하면, 클로저도 동일한 참조를 사용하기 때문에 변경된 값을 참조
  2. 클로저에서 참조된 값
    • let closure = friend.printName는 friend의 참조를 캡처하므로, 실행 시점의 name 값인 "영희"를 출력