- Swift 문법 중 Strong과 Weak의 차이점을 이해한다.
Swift 공부를 진행하면서 Interface Builder에 붙여놓은 객체를 뷰 컨트롤러 클래스의 프로퍼티와 연결하는 과정에서 Storage 설정에 있는 Strong과 Weak이 정확하게 어떤 역할을 하는지 궁금해졌다.
Outlet 어노테이션 객체에 대해 Connection할 때 변수를 strong과 weak으로 세팅하게 되면, weak은 키워드가 들어가지만 strong은 키워드 없이 선언되는 것을 볼 수 있다.
대체 어떤 효과를 지녔길래, 이렇게 두 가지 기능으로 분류해놓은걸까?
결론부터 말하자면 weak타입으로 선언한 변수는 strong타입과 달리 다른 곳에서 참조되고 있다 하더라도 메모리에서 제거될 수 있다.
반대로 말하자면, strong타입은 다른 곳에서 참조되고있다면 메모리가 회수되지 않는다는 뜻이다.
듣고보면 weak이 그다지 메리트가 없어보이겠지만, 메모리 누수에 있어서 weak은 효과적인 키워드가 될 수 있다.
iOS 유튜버
iOS Academy
의 Memory Leak 영상 코드 일부를 차용했습니다.
FirstViewController(줄여서 FirstVC)가 있다고 하자
import UIKit
import Then
// 루트 뷰컨트롤러
final class FirstVC: UIViewController {
private let button = UIButton().then {
$0.setTitle("Go to other View", for: .normal)
$0.setTitleColor(.tintColor, for: .normal)
}
override func viewDidLoad() {
super.viewDidLoad()
// 버튼 위치 설정
button.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
button.center = view.center
// 버튼 액션 설정
button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
// 뷰에 버튼을 추가
view.addSubview(button)
}
// 버튼 클릭 이벤트
@objc private func didTapButton() {
let vc = SecondVC()
present(vc, animated: true)
}
}
FirstVC
의 코드를 간단하게 해석하자면, 버튼을 프로퍼티로 두고 있으며, 버튼을 탭할 시 SecondVC
라는 ViewController를 띄우는 처리를 맡고있다.
SecondVC는 하기 코드와 같다.
//MARK: - CustomView
final class CustomView: UIView {
let vc: UIViewController
init(vc: UIViewController) {
self.vc = vc
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError()
}
}
//MARK: - SecondVC
final class SecondVC: UIViewController {
var myView: CustomView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
self.myView = CustomView(vc: self)
}
}
SecondVC
는 myView라는 CustomView 프로퍼티를 갖고 있고, CustomView를 생성할 때 자기 자신(SecondVC
)을 인자로 전달한다.
CustomView
는 vc라는 ViewController프로퍼티를 갖고 있다.
따라서 SecondVC
가 생성될 때 CustomView와 함께 상호참조하고있음을 알 수 있다.
백문이 불여일견, 한번 실행하여 결과물을 살펴보자
결과물을 볼 때는 전혀 문제가 없어보인다. 단지 버튼을 누르면 빨간 백그라운드를 하고있는 SecondVC
가 튀어나오고, 내리면 사라지면서 다시 FirstVC
로 초점이 맞춰진다.
하지만 여기에는 문제가있다. 방금 언급했다시피 SecondVC
와 CustomView
는 서로 상호참조하고있기 때문에 FirstVC로 다시 넘어갈 때 View가 dismiss되어 사라져도 메모리를 회수하지 못하게 된다. 계속 이러한 사이클을 유지한다면, 메모리 누수가 발생하게 된다.
SecondVC 내부에 deinit 키워드와 viewDidLoad 내부에 print문을 작성하여 메모리에서 해제되는지를 확인해보자.
Note: 아래 코드에서 나오는 키워드를 모르는 분들을 위해
deinit
은 인스턴스가 메모리에서 해제될 때 실행됩니다.
#function
키워드는 실행되고있는 주체의 이름을 출력합니다.
final class SecondVC: UIViewController {
// codes ...
// 이하 생략
// deinit 키워드 추가
deinit {
print("SecondVC Deinit")
}
override func viewDidLoad() {
super.viewDidLoad()
print("SecondVC: \(#function)")
// codes ... 생략
}
}
Deinit이 출력되지 않는 모습이 보인다.
위 코드 중 MyView와 SecondVC를 다시 살펴보자
final class CustomView: UIView {
let vc: UIViewController
init(vc: UIViewController) {
self.vc = vc
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError()
}
}
final class SecondVC: UIViewController {
var myView: CustomView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
self.myView = CustomView(vc: self)
}
}
우리는 이제 두 클래스가 서로 상호참조를 하기 때문에 메모리 누수가 발생하고있다는 것을 인지하고있다. 이를 해결하기 위해 이제 weak
키워드를 넣을 차례이다. 상호참조하고 있는 프로퍼티 중 한 곳에 weak이라는 키워드를 추가해보자.
// var myView: CustomView?
weak var myView: CustomView?
// or
// let vc: UIViewController
weak var vc: UIViewController?
참고로 weak을 선언하게 되면 메모리가 회수될 수 있기 때문에,
let
을var
로, 타입은옵셔널
형태로 정의해주어야한다.
class MyView: UIView {
weak var vc: UIViewController? // weak으로 설정, 옵셔널 형태로 정의
init(vc: UIViewController) {
self.vc = vc
super.init(frame: .zero)
}
// more codes...
}
영원히 메모리에서 제거되지 않던 코드가 weak
설정을 해주었다는 이유로 시스템에 의해 임의로 제거가 되어 상호 참조로부터 벗어날 수 있게 되었다.
그럼 weak가 어떠한 역할을 하기에 상호참조를 벗어날 수 있는 것인지, 다음 글에 좀 더 자세하게 살펴보도록 하겠다.