본문 바로가기
iOS/Swift

클로저의 메모리 구조 + 캡처현상 / 캡처리스트

by 나리._. 2021. 12. 22.

일단 클로저의 메모리 구조를 정리하기 전에

클로저가 무엇인지? 간단하게 정리해보자.

 

클로저는 이름이 없는 함수이다.

 

그럼 클로저는 왜 함수 이름이 필요 없을까? 클로저는 주로 콜백 함수 형태로 사용하기 때문에 이름이 필요 없다.

 

* 여기서 콜백 함수는? 🧐

  1. 다른 함수의 인자로 사용되는 함수
  2. 어떤 이벤트 후 호출되는 함수

그럼 이러한 클로저는 왜 사용될까?

콜백 함수로 클로저의형태가 간단하기도 하고 사후적 정의가 가능하기 때문!

 

클로저가 무엇인지 대충 정리했으니,

이제 클로저의 메모리 구조에 대해 정리해보자!

 

클로저는 참조 형식이기 때문에

클래스처럼 값을 힙 메모리에 저장을 한다.

(클로저의 실제 명령어는 당연히 코드 영역에, 코드 영역을 가리키는 주소는 힙!! 에 저장,

클로저도 함수일 뿐이기 때문에 실행은 당연히 스택 영역)

 

클로저는 참조 타입으로 없어지기 전까지 힙 영역에 존재하며

클로저 외부에 있는 변수를 지속적으로 사용하기 때문에 캡처 현상이라는 것이 발생한다.

 

캡처 현상이란?

클로저를 변수에 할당하거나 클로저를 호출하는 순간,

클로저는 지속적으로 외부 변수를 사용해야 하기 때문에 자신이 참조하는 외부의 변수를 캡처한다.

 

var stored = 0

let closure = { (number: Int) -> Int in
    stored += number
    return stored
}

closure(3) // 3

closure(4)   // 7

closure(5) // 12

stored = 0

closure(5)   //  5

 

위의 코드를 보면 클로저에서 stored 변수를 사용하기 때문에

힙에 저장되는 클로저에 stored 변수 주소를 캡처한다.

 

 

그럼 캡처 리스트는 무엇일까?

 

- 밸류(Value) 타입 캡처와 캡처 리스트

 

캡처 현상

클로저는 외부의 value 타입 변수 주소를 캡처함

var num = 1

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

num = 7
valueCaptureClosure()   // 7

num = 1
valueCaptureClosure() // 1

valueCaptureClosure에 클로저의 메모리 주소가 담겨있음

-> 힙의 클로저에는 함수&num 변수의 주소가 있음 

,  자체를 복사해서 가지고 있는 것이 아니고, num 주소를 캡처해서 계속 사용

 

 

캡처 리스트 사용 -> [캡처할 변수]

클로저 외부의 밸류 타입의 값을 복사해서사용!

let valueCaptureListClosure = { [num] in      // 캡처리스트에서 밸류(value) 타입 캡처
    print("밸류값 출력(캡처리스트): \(num)")
}

num = 7 // main 함수 안에서는 num = 7
valueCaptureListClosure()      // 클로저에서 저장한 1 출력

밸류 타입의 값을 캡처함 (캡처 리스트)

즉, 값 자체를 복사해서 가지고 계속 사용 -> 메모리 주소 캡처 x

 

 타입에서는 외부적인 요인에 의해 참조하는 값이 변경되는 것을 방지하고자 할 때 사용된다 !!!

 

 

- 참조(Reference) 타입 캡처와 캡처 리스트

x는 캡처 리스트 이용 / y 캡처리스트 x

class SomeClass {
    var num = 0
}

var x = SomeClass()
var y = SomeClass()

print("참조 초기값(시작값):", x.num, y.num)

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

x.num = 1
y.num = 1

//x = SomeClass()
//y = SomeClass()

print("참조 초기값(숫자변경후):", x.num, y.num)      

refTypeCapture()                             

print("참조 초기값(클로저실행후):", x.num, y.num)

위의 코드를 실행하면 아래와 같이 결과가 나온다.

 

x는 캡처 리스트 이용 -> 클로저 x에는 x 주소 값이 그대로 복사되어온다.

y 캡처리스트 x -> 클로저 y에는 y변수의 주소값이 저장된다.

 

정리를 하자면!

캡처 리스트 x -> 클로저 외부의 참조 타입 변수 주소를 캡처함 (참조)

캡처 리스트 o -> 클로저 외부의 참조 타입 주소 값을 복사해서 사용 -> 가리키는 인스턴스의 RC를 올라가게 함

 

결국 x, y 둘 다 힙에 있는 해당 객체를 가리키고 있음!

-> x x 직접 참조하여출력하는 것이고

-> y y변수를 거쳐 y 출력하는 것이다!

 

 

그러면 이러한 캡처 리스트를 사용하는 이유는 무엇일까?

1. 값 타입은 값을 복사해서 캡처 - 외부 요인에 의한 값 변경을 방지!

2. 참조 타입은 캡처리스트 내에서 weak, unowned 참조 선언 (강한 참조 해결)

 

 

1번은 대충 이해가 되었다!

2번은 무슨 말일까..? 이거에 대해 자세히 정리해보잣!

 

클로저의 강한 참조

class Dog {

    var name = "초코"

    func doSomething() {
        // 비동기적으로 실행하는 클로저
        // 해당 클로저는 오래동안 저장할 필요가 있음 ==> 새로운 스택을 만들어서 실행하기 때문
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
}

var choco = Dog() // RC:1
choco.doSomething() // RC2

-> 클로저가 choco인스턴스에 대해 강한 참조를 한다.

클로저 내에서 객체의 속성 및 메서드에 접근 시에는 강한 참조를 하고 있다는 것을 표시하기 위한 목적으로 "self키워드"를 반드시 사용해야 한다. 또한 강한 참조 시에는 RC가 +1 올라간다.

 

* self키워드

 1) self.name

 2) [self]  =====> Swift 5.3 이후

 구조체의 경우, self를 생략하는 것도 가능 (Swift 5.3 이후) 

 

해결:  캡처 리스트 내에서 weak, unowned 참조 선언

 

class Dog {

    var name = "초코"
    
    func doSomething() {
        DispatchQueue.global().async{ [weak self] in
            print("나의 이름은 \(self?.name)입니다.")
        }
    }
}

var choco = Dog() // RC:1

choco.doSomething() // RC:1

위와 같이 [weak self]를 이용하여 강한 참조를 해결할 수 있다.

 

마지막으로 강한 참조, 약한 참조를 정리한 코드를 살펴보자.

class Person {
    let name = "홍길동"
    
    func sayMyName() {
        print("나의 이름은 \(name)입니다.")
    }
    
    // 강한참조
    func sayMyName1() {
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
    
    // 약한참조
    func sayMyName2() {
        DispatchQueue.global().async { [weak self] in
            print("나의 이름은 \(self?.name)입니다.")
        }
    }
    
    // 약한참조 옵셔널 벗기기
    func sayMyName3() {
        DispatchQueue.global().async { [weak self] in
            guard let weakSelf = self else { return }   // 가드문 처리 ==> 객체없으면 일종료
            print("나의 이름은 \(weakSelf.name)입니다.(가드문)")
        }
    }
}


let person = Person()

person.sayMyName()
person.sayMyName1()
person.sayMyName2()
person.sayMyName3()

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있다.

 

 

여기까지 클로저, 캡처 현상,  캡처 리스트 정리 끝 ~~!