大器晩成

캡처리스트 본문

iOS/Swift 문법

캡처리스트

zerobugpark 2025. 3. 3. 17:08

 

일전에 중첩함수에서 함수를 리턴하는 경우 클로저의 캡처 현상과 동일하다고 했습니다.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 100)
incrementByTen() // 100
incrementByTen() // 200
incrementByTen() // 300

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen() // 400
  • 위 예시를 보면 함수를 리턴하고 있으며,  변수에 함수를 담았기 때문에, 해당 변수는 heap 영역에 저장되며, runningTotal이 heap에 계속 머무르고 있기 때문에, 값이 0으로 초기화되지 않고 이전 값이 남아있는 현상이 캡처 현상입니다.

[캡처리스트의 형태]

// 1. 파라미터가 없는 경우
 { [캡처리스트] in
 
 }
 
 
 // 2. 파라미터가 있는 경우
 { [캡처리스트] (파라미터) -> 리턴형 in
 
 }

 

 

[값 타입의 캡처]

var num = 1

let valueCapture = {
    print("밸류 값 출력: \(num)")
}

num = 7
valueCapture() // 밸류 값 출력: 7
num = 1
valueCapture() // 밸류 값 출력: 1
  • 밸류타입의 참조(메모리 주소)를 캡처합니다.
  • 즉, 값 자체를 복사해서 가지고 있는 것이 아니고, num의 주소를 캡처해서 계속 사용하고 있습니다.

 

[값 타입의 캡처리스트]

var num = 1


let valueCaptureList = { [num] in
    print("밸류 값 출력: \(num)")
}

num = 7
valueCaptureList() // 밸류 값 출력: 1
  • 변수의 주소를 복사하는 것이 아닌 값 자체를 복사해서 힙 영역에 담고 있습니다.
  • 그렇기 때문에 num의 값이 변경되어도 이전의 값을 가지고 있기 때문에, 클로저에서는 이전 값이 출력됩니다.
  • 값 타입의 캡처는 waek, unowned를 사용할 필요가 없습니다.

*캡처현상: 클로저는 힙의 영역에 존재해야 하고, 클로저 내부에서 외부에 존재하는 변수를 계속 사용해야 하기 때문에 캡처 현상이 발생합니다.

 

[캡처리스트 사용이유]

  • 값 타입은 값을 복사/캡처 (외부적인 요인에 의한 값 변경 방지)
  • 참조타입은 캡처리스트 내에서 (메모리 주소를 캡처) weak / unwoned 참조 선언이 가능 (강함 참조 해결이 가능)

 

[참조 타입 캡처와 캡처 리스트]

class Bird {
    var name = "none"
}

var sparrow = Bird()
var eagle = Bird()

print("참조 초기값:", sparrow.name, eagle.name)

let refTypeCapture = { [sparrow] in
    print("참조 출력값(캡처, 캡처리스트):", sparrow.name, eagle.name)
}

sparrow.name = "참새"
eagle.name = "독수리"

refTypeCapture() // 참조 출력값(캡처, 캡처리스트): 참새 독수리

  • eagle은 단순 캡처 현상이고, sparrow는 캡처리스트 현상이지만, 현재 동작은 동일합니다.
  • 값 타입의 경우 캡처리스트와 캡처 현상이 다르게 발생했지만, 참조 타입은 차이가 없이 동일하게 동작합니다.
  • 하지만 내부적으로 보면, 차이가 있습니다. sprrow는 캡처리스트 현상으로 sprrow의 주소를 복사해서 담고 있으며,  직접적으로 객체의 주소를 캡처하고 있습니다.
  • eagle 스택영역에 있는 eagle의 주소(변수의 주소)를 가리키고 있습니다.
  • 이렇게 인스턴스를 가리키는 경우 강한 참조 사이클이 발생할 수 있습니다.

 


[강한 참조 사이클 문제의 해결]

var sparrow = Bird()

let refTypeCapture = { [weak sparrow] in
    print(sparrow?.name)
}

let refTypeCapture2 = { [unowned sparrow] in
    print(sparrow.name)
}
  • 약한 참조를 할 경우 가리키는 인스턴스가 사라질 경우 nil을 할당할 수 있어야 하기 때문에 옵셔널 타입으로 변경됩니다.
  • weak / unowned를 사용하지 않으면 강한 참조입니다.
  • 위의 예제에서는 클래스 내에서 클로저를 가리키고 있지 않아 강한 참조 사이클이 발생하지는 않습니다.
  값 형식 참조 형식
캡처현상  클로저는 힙의 영역에 존재해야 하고,
클로저 내부에서 외부에 존재하는 변수를 계속 사용해야 하기 때문에 캡처 현상이 발생합니다.
캡처리스트 외부의 값을 복사해서, 내부에 저장하고 사용 외부의 참조 타입의 주소값을 복사해서, 내부에 저장하고 사용
weak, unowned참조 선언 가능

 

 

 

 

[참고]

  • 캡처 리스트 내에서 바인딩하는 것도 가능합니다.
let refTypeCapture = { [weak test =  sparrow] in
    print("참조 출력값(캡처, 캡처리스트):", test?.name)
}

 

 

참고자료

https://alstn38.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%A4%91%EC%B2%A9-%ED%81%B4%EB%A1%9C%EC%A0%80%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0-%EB%B0%8F-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0

 

[트러블슈팅] 중첩 클로저로 인한 순환 참조 및 메모리 누수 해결하기

안녕하세요! 개발콩입니다.오늘은중첩 클로저로 인한 순환 참조되어 메모리 누수가 발생되는 문제를 해결한 글에 대해서 작성하고자 합니다.  문제 상황output.updatePersonData .bind(to: tableView.rx.item

alstn38.tistory.com

 

728x90