- 메모리관리(weak self와 guard의 만남)2025년 01월 12일
- 2료일
- 작성자
- 2025.01.12.:27
기본 개념인 weak, unowned에 대한 자세한 설명은 스킵하겠다. 여기서 다룰것은 언제 그래서 메모리누수가 생기고 댕글런포인트가 뭐고 언제 댕글런포인트가 발생할 수 있을지, weak self는 언제써야하는지.. 등 좀더 실무적인 개념에서 다뤄보려 한다.
weak self를 사용하게 되면 ARC가 1증가하지 않는 약한참조이지만 값이 있을수도 있고 없을수도 있어 guard let같은 옵셔널 binding을 사용하여 코드에 사용해야한다.
@escaping & nonEscaping
nonescaping 클로저가 범위 내에 실행되어, 코드를 즉시 실행하며 나중에 저장하거나 실행할 수 없다
@escaping 클로저는 저장 될 수 있고, 다른 클로저로 전달될 수 있으며 미래의 어느 시점에서 실행 될 수 있다.
딜레이가 부여되는 동작에 @escaping안 붙히면 에러. 그리고. property에 저장하는 경우에도 안붙히면 에러.
class Ho { private var closureEscaper: ((String) -> ())? func escape(closure: @escaping (String) -> ()) { print("escaping!") closureEscaper = closure } }
에 대해 어떤분이 정리해주신 자료이다. 자료의 출처는 맨끝에 남겨놓겟다.
지연된 할당해제란? != 강한 참조순환
가장 왼쪽에를 보면 @escaping이든 아니든 모두 함께 여기에 올 수 있는것을 확인 할 수 있다. 이는 의도적으로 해당 객체의 deallocation을 지연시키는 경우다. 지연 할당 해제는 원치 않는 동작을 초래할 수 잇는 부작용이다. 예를 들어, 뷰컨트롤러를 해제했지만, 해당 메모리가 모든 대기 중인 클로저나 작업이 완료될 때까지 해제되지 않는 경우! 이에 해당한다.
클로저 범위를 유지하는 시나리오는 총 4가지가 있다고 한다.
1. 클로저는 값비싼 연속 작업을 수행하여 모든 작업이 완료될 때까지 범위가 돌아오는 것을 지연시킬 수 있다.
2. DispatchSemaphore같은 스레드 차단 매커니즘을 사용하면 클로저 범위의 반환이 지연되거나 방지될수 있다.
3. Escaping Closure가 지연 실행되도록 예약 된 경우(DispatchQueue.asyncAfter or UIViewPropertyAnimator.startAnimation(afterDelay:))
4. Escaping Closure가 긴 타임아웃을 가진 콜백을 기다리는 경우
func performRequest() { let url = URL(string: "http://localhost:81")! // 81번 포트는 막혀 있음 let session = URLSessionConfiguration.default session.timeoutIntervalForRequest = 999.0 session.timeoutIntervalForResource = 999.0 let session = URLSession(configuration: session) let task = session.downloadTask(with: url) { data, _, error in // 클로저 내부에서 self 참조 guard let data = data else { return } let contents = (try? String(contentsOf: data)) ?? "No contents" print(contents) print(self.view.description) } task.resume() }
URLSession을 이용하여 port 81에 요청을 보내는데, 이 포트는 차단되어 있어 타임아웃이 발생한다.
요청 타임아웃은 999초로 설정.
클로저에서 self를 참조했으며, weak, unowned 키워드 사용되지 않음.
중요하게 봐야할 것은 강한 순환참조를 일으키는 코드는 아니다. 보면 알수있음. 다른 객체를 참조하지는 않으니까. 하지만 downloadTask를 종료하지 않고 뷰컨을 dismiss하면 경고가 뜰것이다.
이것은 저 4가지 경우중 4번의 케이스다. 이 클로저는 호출되거나 시간 초과 기한에 도달할 때까지 또는 작업이 취소될 때까지 그 안에 참조된 모든 개체(이 경우 자체)에 대한 강력한 참조를 유지한다. 그러면 여기에 weak 나 Unowned를 사용할 수 있다.
하지만 unowned를 사용하면 참조대상이 이미 메모리에 없을때는 crash가 생긴다. 이 경우는 controller가 dismiss되어도 클로저는 여전히 실행을 하고 있기에 self가 없기에 런타임 에러가 뜬다.
댕글런 포인터(Dangling Pointer)
위의 경우가 바로 댕글런 포인터의 예다. 이미 해제된 메모리를 참조하는 포인터를 말한다.
class NetworkManager { func fetchData(completion: @escaping (String) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + 5) { completion("Data received") } } } class ViewController { let networkManager = NetworkManager() func startFetching() { networkManager.fetchData { [unowned self] data in print("Received data: \(data)") } } } var vc: ViewController? = ViewController() vc?.startFetching() vc = nil // ViewController가 해제됨
VC가 사라졌지만 5초후 클로저가 실행되면서 unowned self가 댕글런 포인터가 발생하는 예시다.
그래서 일반적으로 unowned는 정말 개발자가 수명을 제대로 체크했을때 사용하고, 그외는 weak self를 일반적으로 사용한다.
다시 weak self이야기로 넘어가자.
weak self는 값이 있을 수도 없을수도 계속해서 추적하고 있기에 optional self?다. 그래서 일반적으로 guard let을 통해 optional Binding을 한다. 그런데 주의해야할 점이 있다고 한다.
이전에 말했던 지연 할당 해제가 이루어지는 경우중 1번과 2번을 다시 살펴보자. 1번은 비싼 연속작업의 경우고 2번은 쓰레드 블락하는 경우다.
networkManager.fetchData { [weak self] data in guard let self = self else { return } // ① 여기서 강한 참조 생성 self.processData(data) // ② 클로저 내 비싼 작업 시작 self.updateUI() // ③ UI 업데이트 }
2번째줄에서 클로저의 중간에 비싼 작업(예: 이미지처리, 대규모 데이터 계산) 등이 포함되어 잇는 경우 이 작업이 끝날때 까지 self가 해제되지 않는다. 만약 해당 클로저가 뷰 컨트롤러가 해제된 이후에도 실행 중이라면 불필요한 작업을 계속해 수행하게 된다.
guard let 은 클로저가 끝날때까지 self를 강하게 참조하기 때문에 뷰컨트롤러를 즉시 해제하고 싶을 때 적합하지 않는다.
networkManager.fetchData { [weak self] data in self?.processData(data) // ① 여기서 self가 nil이면 이 라인 스킵 self?.updateUI() // ② 여기서도 self가 nil이면 이 라인 스킵 }
여기서는 클로저가 실행되는 중간에 self가 해제되더라도 이후의 작업은 수행하지 않는다. 결국 불필요한 작업을 수행하지 않고 즉시 중단되기 때문에 메모리 효율성이 올라간다. 결국 self?.는 클로저내에 self가 nil일 가능성을 고려하며 특정 작업이 반드시 실행되어야 할 필요가 없는 경우에 적합하다. 그렇다면 weak self랑 guard문을 결국 함께쓰면 안되는거 아냐? 라는 궁금증이 들었다. 특히 그러면 strong reference랑 똑같은 거 아냐?
강한 참조는 클로저 생성 시점에 self를 강한 참조로 캡쳐한다.
강한참조는 클로저 전체 스코프에 걸쳐 유지되며, 클로저가 실행되는 동안 self해제 X. 그래서 클로저 실행 중에 self메모리 해제 지연.
하지만 weak self와 guard문을 썼을 경우 클로저 실행되기 전에 self가 이미 Nil이라면 강한 참조로 전환하지 않고 즉시종료한다. 또한 클로저 내부에서만 일시적으로 강한 참조고 클로저 끝나면 해제된다.
- 데이터 무결성이 중요한 작업 : 작업 도중 self가 해제되면 데이터 손상이 발생할 수 있는경우
--> 결국 weak self와 guard의 만남은 객체 해제되기전에 모든 작업을 끝내고 싶을 때 사용하자
그렇다면 [weak self]가 필요하지 않은경우는 언제일까?
1. GCD
참조 순환이 발행하지 않은 경우는 코드가 즉시 실행(클로저에서 GCD에 즉시 전달되고 실행 스케쥴에 따라 처리된다는 뜻)되어 self에 대한 참조가 제거된다.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.view.backgroundColor = .red } DispatchQueue.main.async { self.view.backgroundColor = .red } DispatchQueue.global(qos: .background).async { print(self.navigationItem.description) }
GCD를 사용할때 비동기 작업은 클로저를 실행 대기열에 넣는다. GCD의 실행 대기열에 추가된 클로저는 GCD에 의해 관리되는것!. 작업이 완료되면 GCD는 클로저에 대한 참조를 해제. 이 구조 덕분에 클로저가 대기열에서 끝나면 강한 순환참조가발생하지않는다. 내가 헷갈렷던 부분이 저기인거 같다. 그러면 데드라인을 1000초뒤에 실행하면 그 전에 뷰를 dismiss하면 메모리 릭이 발생할 수 있는거 아닌가? 하지만 잘 생각해보면 1000초뒤에 알아서 self를 반환하기에 순환참조이슈는 아니기에 메모리 릭이 발생하지않는다. 그냥 메모리 해제가 지연되는것이다.!!! 뭐 좋은 코드는 아니겟지만./
class MyClass { var workItem: DispatchWorkItem? func setupWorkItem() { workItem = DispatchWorkItem { self.doSomething() // 강한 참조 순환 발생 } } }
이 경우에서는 GCD작업을 나중에 처리하기 위해 클로저 내부에서 self를 강한 참조로 캡처하게 되면 참조순환이 발생한다.
2. UIView 애니메이션.
UIView.animate(withDuration: 0.3) { self.view.alpha = 0.0 // [weak self] 없이 사용 가능 }
애니메이션 클로저는 즉시 실행되며, 애니메이션이 끝나면 클로저에 대한 참조도 사라지기에.
객체 속성에 함수를 저장하는 경우
자 이번에는 객체간에 클로저나 함수를 전달하여 다른 객체의 속성으로 저장하는 경우를 살펴보자. DelegatePattern의 경량화 버전으로 한 객체(A)가 다른 객체(B)의 메서드를 호출할 수 있다. 하지만 이 과정에서 메모리 누수가 발생 할 수 잇다.
class PresentedController { var closure: (() -> Void)? }
이 객체는 클로저를 프로퍼티로 저장.
class MainController { var presentedController = PresentedController() func setupClosure() { presentedController.closure = printer } func printer() { print("MainController description") } }
MainController라는 객체가 있고 여기서 자신의 메서드를 PresentedController의 클로저 속성에 전달.
printer메서드는 MainController의 메서드인데, 이 메서드를 presentedController.closure에 할당하고 있다. 그리고 함수 자체를 할당하기에 ()를 붙이지 않았다. 결과적으로 PresentedController에서 closure를 호출하면 MainController의 printer 메서드 실행되며
MainController description이 프린트된다.
하지만 이 경우 강한 참조 순환이 발생할 수 있다
closure는 self.printer를 참조하고 MainController는 PresentedController를 소유하며, PrsentedController는 다시 클로저를 소유.
MainController → PresentedController → closure → MainController
weak self의 위치에 따른
let workItem = DispatchWorkItem { // <-- first closure UIView.animate(withDuration: 1.0) { // <-- second closure self.view.backgroundColor = .red } } self.workItem = workItem
일반 적으로 강한 참조 순환을 막기 위해 weak self를 사용한다 가정했을때 해당 코드에서 두개중 고민을 할 것이다.
하지만 2번에 넣으면 문제가 있다.
self와 DispatchWorkItem의 강한 순환 참조 관계. self가 workItem을 강하게 참조. 반대로 workItem의 클로저 내부에서 self를 참조하므로, self와 workItem이 강한 순환 참조가 발생.
2번에 넣어도 여전히 self를 강하게 참조하기에. 그래서 바깥에 넣어야한다
와.. 맨밑의 참고자료를 이용하여 글을 썼다. 나도 모르는 부분이 너무 많았다. 이러니 털리지 항상 ㅠㅠ 다음게시글은 그래서 어떻게 메모리 관리를 볼 수 있는 instruments 사용법과 내 코드를 어떻게 보완하면 좋을지 더 실무적인 자료로 나타나겠다 안녕
https://medium.com/@almalehdev/you-dont-always-need-weak-self-a778bec505ef
'면접준비' 카테고리의 다른 글
Hash-Hashable을 곁들인 (1) 2025.01.05 Image(Pixel, asset, memory....)등등을 포함한 & HIG (1) 2024.11.24 SilentPush&RichPush (2) 2024.11.08 test Code (with TCA) (0) 2024.11.04 CLMonitor (1) 2024.10.22 다음글이전글이전 글이 없습니다.댓글