클로저와 캡처리스트

kimhyeji·2023년 12월 20일

클로저(Closure)

클로저란, 이름이 없는 함수 == 익명 함수.

함수와 기능은 완전히 동일한데 형태만 다를 뿐입니다.

  • 함수: 이름이 있는 코드 묶음

  • 클로저: 이름이 없는 코드 묶음, 이름이 없어도 호출할 수 있는 형태이기 때문에 이름이 필요 없음

Swift는 함수를 “일급객체”로 취급합니다. 즉 함수도 타입으로 취급합니다.

  • 함수를 변수에 할당할 수 있습니다.
  • 함수를 실행할 때, 함수를 파라미터로 전달할 수 있습니다.
  • 함수를 반환할 수 있습니다.

→ 일급객체의 의미는 변수에 할당이 가능하고, 파라미터로 전달할 수 있고, 반환할 수 있다는 것입니다.

클로저를 사용하는 이유

  1. 이름이 필요 없습니다.

    함수를 실행할 때 전달하는 의미로 정의되기 때문에 클로저는 이름이 필요 없습니다.

    → 함수를 파라미터로 받는 함수를 실행할 때, 클로저를 정의함과 동시에 파라미터로서 전달을 할 수 있기 때문에 이름이 필요없고 그래서 클로저를 사용합니다.

    그리고 파라미터로 전달하는 함수를 “콜백함수”라고 부릅니다.

  2. 사후적으로 클로저를 정의하고 전달할 수 있습니다.

    미리 어떤 함수를 정의하고 실행하는 것이 아니라 클로저를 정의를 하면서 전달을 하는 것이 사후적으로 정의할 수 있다는 것입니다.

    그리고 사후적으로 클로저를 정의하고 바로 전달할 수 있음이 클로저를 사용하는 이유입니다.

클로저의 메모리

클로저는 참조타입입니다.

→ 참조타입은 힙에 저장되고, 힙의 주소가 스택에 저장됩니다.

CPU에 대한 명령어의 묶음이기 때문에 코드 영역에 있는 코드 묶음의 주소가 힙에 저장됩니다. 즉, 컴파일된 명령어의 메모리 주소가 힙에 저장됩니다.

그리고 실제로 클로저가 실행되면 스택 프레임에 만들어지면서 동작하고, 동작이 끝나면 사라진다. 힙에서는 주소를 저장하고 있는 것 뿐.

→ 함수의 직접적인 실행은 스택 프레임에서 동작합니다!!

클로저의 캡쳐(Capturing Value)

클로저 외부에 존재하는 변수를 내부에서 사용하기 때문에 외부 변수를 계속 사용하기 위해 값을 저장하거나 참조를 저장합니다.

즉 저장할 필요가 있는 값의 참조를 저장하여 지속적으로 해당 값을 참조할 수 있도록 만들어줍니다.

실제로 클로저가 실행되면 스택 프레임에서 캡쳐한 값의 주소를 참조하여 값을 사용하고 실행이 끝나면 스택 프레임이 사라지고 캡쳐한 값을 다시 저장해둔다.

func calculate() -> ((Int) -> Int) {
	var sum = 0
	func square(num: Int) -> Int {
		sum += (num * num)
		return sum
	}
}

var square = calculate()

square(10)
square(20)
square(30)

내부 중첩 함수를 반환하고 있는 함수를 변수에 할당하는 순간, 반환된 함수가 변수에 저장이 되고 즉 변수에 클로저가 할당이 됩니다. 이때 내부 중첩 함수였음에도 중첩 함수 기준에서 명령어를 실행하기 위해 외부 값을 저장해야 하기 때문에 캡쳐 현상이 발생하게 됩니다. 이것은 클로저의 캡쳐 현상과 동일합니다.

이렇듯 함수를 변수에 저장하면 굳이 클로저가 아니더라도 클로저와 같이 동작하기 때문에, 반드시 힙에 저장됩니다.

그리고 함수 뿐만 아니라 함수 실행에 필요한 외부 변수도 함께 힙에 저장합니다. 그리고 이렇게 힙에 저장된 클로저의 실행을 위해 필요한 외부 변수도 함께 저장하는 것을 클로저의 캡쳐 현상이라고 합니다.

변수에 할당된 클로저의 실행이 종료가 됨에도 변수가 계속 존재하기 때문에 클로저 실행을 위한 스택 프레임만 사라졌을 뿐 스택에서 클로저의 힙 메모리 주소를 계속 가리키고 있기 때문에 캡쳐한 외부 값 또한 계속 힙에 저장되어 있습니다.

즉 일반 함수와 달리 클로저는 스택 프레임을 쌓았다가 사라지는 것이 아니라 힘에 지속적으로 메모리 공간을 차지하고 값을 저장하고 있습니다. 그렇기 때문에 누적적으로 값이 변경됩니다.

캡처리스트

캡처리스트는 약한 참조와 비소유 참조 선언이 가능하도록 도와주는 것으로, 강한 참조를 해결할 수 있습니다.

{ [캡처리스트] (파라미터) in
	// ...
}
  1. 캡처리스트에서의 값 타입 캡처

    클로저에서 외부 변수의 값에 변화를 줌으로써 캡처 현상이 발생하고 변수의 주소를 참조하고 있기 때문에 해당 변수가 바뀌면 바뀐 값을 계속해서 참조하고 있지만, 캡처리스트의 경우 값을 복사해서 캡처했기 때문에 외부 변수의 값이 다시 변경되어도 주소를 참조하고 있지 않기 때문에 클로저 내부에서는 여전히 복사한 값 그대로를 사용합니다.

  2. 캡처리스트에서의 참조 타입 캡처

    캡처리스트로 참조타입을 캡처하는 경우 일반적으로 캡처 현상이 발생하는 것처럼 메모리 주소를 가리키고 있기 때문에 값의 변화가 그대로 클로저 내부에서도 반영이 되지만 어떤 주소를 캡처하여 가리키고 있는지가 다릅니다. 일반적인 캡처 현상에서는 참조 타입이 할당된 변수의 메모리 주소를 가리키고 있지만 캡처리스트에서는 직접 참조 타입 객체 자체의 주소를 가리키고 있습니다. 한 번 거처셔 가리키고 있는지와 직접 가리키고 있는지의 차이라고 볼 수 있습니다.

이렇게 직접 객체의 주소를 가리키고 있는 경우 강한 참조를 하고 있는 것이며, 강한 참조 사이클이 발생할 가능성이 생깁니다. 그리고 이 문제를 해결하기 위해 캡처리스트에 weak/unowned 키워드와 함께 선언합니다.

정리하자면, 클로저 내부에서 외부에 존재하는 변수를 계속 사용해야 하기 때문에 캡처 현상이 발생하는데, 이때 값 타입의 경우 외부적인 요인에 의한 값 변경 방지를 위해 캡처리스트를 사용해 값을 복사하고 참조 타입의 경우 캡처리스트로 선언하면 강한 참조로 인한 문제가 발생할 수 있기 때문에 캠처리스트 내에서 weak/unowned 참조 선언을 하여 해결할 수 있습니다.

0개의 댓글