클로저 캡쳐 매커니즘
클로저 캡쳐 매커니즘
[Swift] 클로저 캡처: 내가 알던 값이 왜 아니지? (Value vs Reference Capture)
Swift 개발을 하다 보면 비동기 처리나 콜백 함수에서 예상과는 다른 값이 출력되어 당황하는 경우가 있습니다. “분명 값을 바꿨는데 왜 옛날 값이 나오지?” 혹은 반대로 “Struct인데 왜 최신 값이 나오지?” 같은 의문들이죠.
오늘은 실제 예제 코드 두 가지를 통해 **클로저의 캡처 리스트(Capture List)**와 메모리 참조 방식을 완벽하게 파헤쳐 보겠습니다.
Case 1: “왜 10이 아니라 0이 나오죠?” (Capture List)
첫 번째 예제는 Int형 변수(Value Type)와 클로저 캡처 리스트를 사용한 경우입니다.
1
2
3
4
5
6
7
8
var counter = 0
// [counter]를 통해 값을 '복사'해서 캡처함
let closure = { [counter] in
print(counter)
}
counter = 10
closure() // 결과: 0 출력
💡 원인 분석: 캡처 리스트(Capture List)와 Const Value
이 코드의 핵심은 클로저 선언부의 대괄호 [counter]입니다. 이를 **캡처 리스트(Capture List)**라고 부릅니다.
- 복사(Copy) 발생 시점:
[counter]를 작성하는 순간, 클로저는 **“클로저가 생성되는 시점”**의counter값(0)을 상수(Constant)로 복사하여 자신만의 영역에 저장해 둡니다. - 독립성: 이후 외부의
counter변수가 10이 되든 100이 되든, 클로저 내부에 이미 복사된counter는 영향을 받지 않습니다.
만약 [counter]를 빼고 아래처럼 작성했다면 결과는 다릅니다.
1
2
3
4
5
6
// 캡처 리스트 없음
let closure = {
print(counter) // 외부 변수 counter의 '주소'를 참조
}
counter = 10
closure() // 결과: 10 출력
캡처 리스트가 없으면 클로저는 기본적으로 **Reference Capture(참조 캡처)**를 하기 때문에, 실행 시점에 외부 변수의 최신 값을 가져옵니다.
Case 2: “Struct인데 왜 업데이트된 값이 나오죠?” (Reference Capture)
두 번째 예제는 Struct(구조체)를 Class(클래스) 안에서 사용할 때 발생하는 혼동입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct User {
var name: String
}
class ViewModel {
var user = User(name: "Green")
lazy var displayName: () -> String = {
// ⚠️ struct는 값 복사라고 생각해서 "Green"이 고정될 것 같지만...
return self.user.name
}
func updateUser() {
user.name = "Updated"
print(displayName())
}
}
결과값: updateUser()를 실행하면 **“Updated”**가 출력됩니다.
💡 원인 분석: self를 캡처했기 때문
많은 분들이 User가 구조체(Value Type)이므로, 클로저가 생성될 때 “Green”이라는 값이 복사되어 박제될 것이라고 오해합니다. 하지만 여기엔 함정이 있습니다.
- 무엇을 캡처했는가?: 위 클로저 안에는 캡처 리스트(
[...])가 없습니다. 따라서 클로저는self(ViewModel의 인스턴스)를 참조 캡처(Reference Capture) 합니다. - 접근 경로: 클로저가 실행(
displayName())될 때의 동작 순서는 다음과 같습니다.- ① 캡처해 둔
self(ViewModel 인스턴스)를 바라봅니다. - ② 그 인스턴스의 현재
user프로퍼티에 접근합니다. - ③
updateUser()에 의해user는 이미 “Updated”로 변경된 상태입니다.
- ① 캡처해 둔
- 결론:
User가 Struct인 것은 중요하지 않습니다. 우리가 캡처한 것은User그 자체가 아니라,User를 들고 있는self이기 때문입니다.
만약 “Green” (변경 전 값)을 유지하고 싶다면?
이때야말로 캡처 리스트를 사용해야 합니다.
1
2
3
4
lazy var displayName: () -> String = { [user] in // [user]로 캡처 리스트 명시
// 클로저 생성 시점의 user(Green)가 '값 복사'됨
return user.name
}
위처럼 작성하면 self를 거치지 않고 user 구조체 자체를 복사해서 저장하므로, 나중에 값이 바뀌어도 “Green”이 출력됩니다.
🚀 핵심 요약 (Cheat Sheet)
| 구분 | 코드 형태 | 동작 방식 | 키워드 |
|---|---|---|---|
| 기본 (Default) | { print(value) } | Reference Capture 실행 시점의 최신 값을 가져옴 | 참조, 최신값 |
| 캡처 리스트 | { [value] in print(value) } | Value Capture (Copy) 클로저 생성 시점의 값을 복사하여 저장 (상수 취급) | 복사, 불변, 스냅샷 |
| 객체 속성 접근 | { print(self.structProp) } | Reference Capture of selfself를 참조하므로, 속성이 바뀌면 바뀐 값을 가져옴 | self 참조 |
개발자를 위한 조언
- 값이 나중에 바뀌더라도 클로저 생성 시점의 데이터를 유지해야 한다면
[변수명]캡처 리스트를 꼭 사용하세요. - Class 내부에서
lazy var클로저를 쓸 때, 내부 프로퍼티가 Struct라도self를 통해 접근하면 언제나 최신 값을 가져온다는 점을 명심하세요.
This post is licensed under CC BY 4.0 by the author.