study record
[Swift] 순환참조 (Retain Cycle) + (클로저 weak self) 본문
순환 참조(retain cycle)이란? 언제 발생하는가? 어떻게 처리해야 하는가?
순환 참조란?
순환 참조는 두 가지 이상의 객체가 서로에 대한 Strong Reference(강한 참조) 상태를 가지고 있을 때 발생하며, 순환 참조가 발생하게 되면 서로에 대한 참조가 해제되지 않기 때문에 메모리에서 유지되며 이로 인해 메모리 릭이 발생하게 된다.
이러한 메모리 누수 문제를 해결하기 위해 weak, unowned reference가 사용된다.
강한 참조
참조의 기본은 강한 참조로, 별도의 식별자를 명시하지 않으면 강한 참조를 한다.
ARC가 해당 인스턴스를 해제하지 않고 유지해야 하는 명분을 제공하는 것이다.
인스턴스를 다른 인스턴스의 프로퍼티, 변수, 상수 등에 할당할 때 강한 참조를 사용하면 참조 횟수가 1 증가하고, nil을 할당하면 참조 횟수가 1 감소한다.
인스턴스는 힙 영역에 할당되고, 지역변수가 인스턴스의 주소값을 할당받아 인스턴스의 참조횟수가 1 증가한다.
인스턴스의 주소값이 변수에 할당될 때, RC가 증가하면 강한 참조이다.
순환 참조
ARC(Automatic Reference Counting)는 인스턴스에 대한 참조의 개수를 추적하고, 인스턴스가 더 이상 필요하지 않게 되면(참조 카운팅이 0이 되면) 할당해제를 실시한다.
두 인스턴스가 서로에 대해 참조를 가지고 있어서 각 인스턴스가 다른 인스턴스를 계속 유지하게 되는 경우를 강한 순환 참조라고 한다. 두 클래스가 클래스의 프로퍼티로 서로의 클래스 인스턴스를 가지고 있으면, 각자의 클래스 인스턴스를 nil로 할당하여도 프로퍼티를 설정하며 참조 카운트가 증가하여 메모리에서 사라지지 않는 것이다.
이와 같이 서로에 대한 강한 참조로 인해 인스턴스를 제대로 해지할 수 없는 상태가 강한 순환 참조 상태이다. ARC는 이러한 강한 순환 참조에 대한 메모리 관리를 해주지 않기 때문에 약한 참조, 비소유 참조를 사용하여 해결해야 한다.
약한 참조 (weak reference)
약한 참조는 인스턴스를 참조할 때, 참조 카운트를 증가시키지 않는다. 참조하던 인스턴스가 메모리에서 해제될 때, 자동으로 nil이 할당되어 메모리가 해제된다. (-1된다.)
약한 참조는 참조하는 인스턴스에 대해 참조를 강하게 유지하지 않아서 ARC가 해당 인스턴스에 대한 참조를 해제할 수 있도록 하여 강한 순환 참조 상태를 막을 수 있다.
약한 참조는 변수 혹은 속성 앞에 weak 키워드를 둠으로써 사용할 수 있다.
약한 참조를 하고 있기 때문에 참조가 되고 있는 동안에도 해당 인스턴스가 할당 해제될 수 있다.
ARC는 인스턴스가 할당 해제될 때 해당 인스턴스를 약한 참조하는 프로퍼티를 nil로 초기화한다. 그렇기 때문에 약한 참조는 항상 옵셔널 변수에만 가능하며, 상수에는 사용할 수 없다.
또한 약한 참조는 언제든지 해제될 수 있기 때문에 reference count를 증가시키지 않는다.
무소유 참조 (unowned reference)
무소유 참조도 인스턴스에 대한 강한 참조를 유지하지 않는다.
참조하던 인스턴스가 만약 메모리에서 해제된 경우, nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있다.
접근하려 하면 에러 발생.
무소유 참조는 다른 인스턴스와 생명주기가 같거나 더 긴 경우에 사용한다.
무소유 참조는 항상 값을 가지고 있다고 가정한다.
무소유 참조는 옵셔널 타입이 아니고, nil로 만들지 않는다.
할당 해제되지 않은 인스턴스를 참조한다고 확신하는 경우에만 무소유 참조를 사용한다.
클로저에서 weak self
약한 참조(weak reference)는 순환 참조로 인한 메모리 누수에서 벗어나기 위해 사용한다.
스위프트에서 ARC를 사용하면서 대부분의 참조문제를 해결하고 있지만, 두 가지 이상의 객체가 서로에 대해 강한 참조(strong reference) 상태를 가지고 있다면 순환 참조 현상이 발생하게 되고 메모리 누수가 일어난다. 메모리 누수가 일어나면 앱에서 Out of Memory 크래시를 내게 되어 이를 방지하기 위해 메모리 누수 이슈를 해결해야 한다.
이를 위해 weak를 사용한다. weak reference는 다음과 같은 특징이 있다.
- 인스턴스를 strong하게 유지하지 않는다. 따라서, weak 참조가 인스턴스에 대한 참조를 유지 중이어도 할당이 해제 될 수 있다.
- weak 참조는 레퍼런스 카운트를 증가시키지 않는다.
- weak 참조는 옵셔널이며, 런타임에도 value를 nil로 만들 수 있다.
따라서 self를 캡처하는 상황에서 self에 대한 순환참조가 발생하지 않기 위해 weak self를 사용한다.
weak self 사용 시점
1. 이스케이핑 클로저 안에서 지연 할당의 가능성이 있는 경우 (비동기 데이터 처리, 타이머 등)
*일반 클로저에서는 즉시 실행되어 강한 참조를 유발하지 않으므로 weak self를 사용할 필요가 없다.
2. 클로저가 객체에 대한 지연 해제 가능성이 있는 경우
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] timer in
guard let self = self else { return }
self.repeatCount += 1
print("Repeat Count: \(self.repeatCount)")
self.timerLabel.text = "\(self.repeatCount)"
})
이런 경우에 weak self를 사용하면서 인스턴스에 대해 레퍼런스 카운트를 증가시키지 않아서 클로저 내부에서는 self를 strong reference로 사용하고, 외부에서는 캡처한 self가 nil이 될 경우 guard 구문에서 return 시키므로 메모리 누수에 대한 걱정 없이 사용할 수 있다.
참고 :
'Swift > 스위프트 정리' 카테고리의 다른 글
[Swift] Swift 메모리 관리 - ARC란? (0) | 2022.05.18 |
---|---|
[Swift] Dynamic Dispatch란? (0) | 2022.04.14 |
[Swift] Delegate와 Retain 여부! (0) | 2022.03.15 |
[Swift] 탈출 클로저(escaping closure)란? (0) | 2022.03.09 |
[Swift] static (0) | 2022.02.26 |