Swift

[Swift] 클로저와 중첩함수의 캡처 현상 (1)

하이D:) 2023. 11. 1. 13:36

Swift에서 일급객체라고 불리는 클로저의 메모리는 어디에 저장되는지 알고 있나요?

그리고 외부 변수가 클로저 내부에서 사용될 시, 함수와는 다르게 어떤 현상이 일어나는지는 알고 있나요?

 

이번 글에서는 클로저중첩함수가 할당된 변수가 실행될 때 캡처 현상이 일어나는 상황과 이유에 대해 정리해 보려 합니다! 다음글에서는 캡처 현상 때문에 외부 요인에 의해 내부에서 사용되는 외부 값이 변경되는 것을 방지하기 위해 어떤 방법을 쓸 수 있는지 방법을 이야기해 볼게요 ㅎㅎ

 

 

[Swift] 클로저와 중첩함수의 캡처 현상 (1)

[Swift] 클로저와 중첩함수의 캡처 현상 (2) | 캡처리스트 클로저에서self키워드

 

 

# 🍑 클로저란 무엇이고 왜 사용할까?

클로저는 "이름이없는(익명) 함수"라고 하죠. Swift에서 함수를 정의할 때 func 함수이름 () {} 이런식으로 정의해 주는데 클로저는 이름도 필요 없고 바로 변수에 할당도 할 수 있는 함수이죠!

 

아래 예시처럼 변수에 할당해서 실행해주고 싶은 함수가 있을 경우, 함수는 정의 - 할당 - 실행 의 과정이 필요하지만 클로저는 정의와 동시에 변수에 할당하고 실행할 수 있어서 특정 경우에는 훨씬 간단하게 쓰일 수 있겠죠?

//정의
func function1 () {
    print("1")
}
//할당
let function2 = function1
//실행
function2()


let function3 = {
    print("3")
}
function3() //클로저 실행

 

그리고 클로저는 콜백함수로 많이 전달하곤 하는데 함수를 파라미터로 전달해야할 때 이름없는 함수인 클로저를 바로 정의해서 전달해 줄 수 있다는 장점이 있어요.

 

파라미터로 전달한다... 이게 가능한 것은 클로저가 "일급객체"이기 때문입니다!

 

그렇담 "일급객체" 란 또..무엇일까요?? 1) 함수에서 전달인자 또는 반환값으로 사용할 수 있고 2) 변수나 데이터 구조 안에 담을 수 있는 것을 뜻합니다! Swift에서는 함수, 클로저, 프로토콜 같은 것들이 있겠네요 ㅎㅎ

func function1 (closure : ()->()) {
    closure()
}

//함수 실행 (후행클로저 적용)
function1{
    print("function1-closure")
}

 

 

# 🍑 클로저의 메모리

그럼 클로저의 메모리는 어디에 위치할까요? 

 

클로저는 클래스와 같은 "참조타입"입니다! 즉, 힙영역에 저장되고 ARC 모델을 통해 메모리 관리가 되는 것이죠. 클로저 안에서 실행되는 명령어의 묶음은 코드 영역에 존재하며 힙영역의 클로저는 그 명령어 묶음을 가리키고 있어요!

 

 

# 🍑 클로저와 중첩함수의 캡처현상

"캡처현상"에 대해 들어보셨나요? 클로저와 캡처 현상에는 어떤 관계가 있고 어떤 상황에서 일어나는지 한번 정리해 보겠습니다!

 

일단 캡처 현상이 일어날 수 있는 상황은 크게 아래 두 가지로 나눌 수 있습니다.

1) 함수의 경우에, 함수가 힙 영역에 존재하는 경우 (함수를 변수에 할당) 함수 외부의 값을 함수 내부에서 사용할 때

2) 클로저의 경우, 외부 값을 클로저 내부에서 사용할 때

 

공통점은 외부 값을 실행문 안에서 사용한다는 것입니다!

 

그럼 도대체 캡처 현상이란 뭘까요 ㅎㅎ

아래 두 가지 코드 예시를 보면 알 수 있듯이 각 함수/클로저를 실행하고 다음에 또다시 실행할 때 우리가 기대하는 값이 출력되지 않고 있죠. 이게 바로 위에서 정의한 두 가지 상황에 부합되어서 캡처현상이 일어나고 있는 것인데요. 함수/클로저 내부에서 사용되는 외부 변수가 이전 함수/클로저 실행에서 할당된 값을 (클로저가 종료됐음에도 불구하고) 계속 기억하고 있다는 걸 볼 수 있습니다.

이렇게 힙영역에 존재하는 함수/클로저가  외부 값을 사용할 때 힙 메모리 내부에 인스턴스의 프로퍼티처럼 외부 값이 함께 존재하게 되는 것을 캡처현상이라고 합니다.

//변수를 캡처하는 함수(중첩 함수의 내부 함수) - 캡처 현상의 발생
func calculateFunc() -> ((Int) -> Int) {
    
    var sum = 0
    
    func square(num: Int) -> Int {
        sum += (num * num)
        return sum
    }
    
    return square
}

// 변수에 저장하는 경우(Heap 메모리에 유지)
var squareFunc = calculateFunc()


squareFunc(10) //100 (O) 
squareFunc(20) //500 (O) 400 (X)
squareFunc(30) //1400 (O) 900 (X)
var num = 0

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

closure(3) //3 (O)
closure(4) //7 (O) 4 (X)
closure(5) //12 (O) 5 (X)

num = 0
closure(5) //5

 

그림으로 표현하면 대충 이렇다고 볼 수 있겠네요??  ( 비루한 그림 실력은 흐린눈...)

다음 그림부터 중점적으로 봐야 하는 건 "클로저에서 사용하고 있는 외부값"이라는 부분입니다!

 

 

# 🍑 값 타입과 참조 타입의 캡처

캡처현상이 일어나는 건 위에서도 말했듯이 두 가지 상황이 있는데 이 글에서는 클로저를 위주로 다루고 있기 때문에 클로저에 한해서 설명을 해보려 합니다!

 

똑같이 클로저에서 일어나는 캡처 현상이라도 "값타입"을 캡처하고 있는가 "참조타입"을 캡처하고 있는가에 따라 또 다른데요!

 

거참.. 저도 캡처 현상을 이해하는데 좀 오래 걸렸는데 값타입과 참조타입 캡처하는 게 또 다르다뇨.. 일단 한번 찬찬히 봅시다아!

 

> 값 타입 캡처

클로저가 값 타입을 캡처할 때는 메모리 주소를 캡처합니다!

그림으로 표현하자면 아래와 같고 코드 예시로 보면 좀 이해가 되시나요?

var num = 1

let valueCaptureClosure = {
    print("값 타입 캡처", num)
}

valueCaptureClosure() //1

num = 3

valueCaptureClosure() //3

 

 

> 참조 타입 캡처

클로저가 참조 타입을 캡처할 때는 인스턴스를 할당한 변수를 캡처합니다!

변수를 한 번 거쳐서 인스턴스 객체를 가리키고 있는 것이죠!

class Class1 {
    var num = 0
}

var x = Class1()
var y = Class1()


let refTypeCapture = {
    print("참조 타입 캡처", x.num, y.num) // 변수를 참조하고 그 변수를 거쳐서 인스턴스 객체를 가리킴
}

refTypeCapture() //0,0

x.num = 1
y.num = 1

refTypeCapture() //1,1

 


지금까지 클로저의 기본적인 개념과 클로저에서 외부 값 사용 시 발생하는 "캡처현상"에 대해서 알아보았는데요. 다음 글에서는 값 타입 캡처 시 외부 요인에 의해 외부 값이 변동되는 것을 방지하기 위해 사용되는 캡처 리스트라는 개념과, 참조타입 캡처 시 캡처 리스트 내부에서 약한 참조를 해주어야 하는 이유에 대해서도 설명해보려 합니다! :)