[문과 코린이의 IT 기록장] IOS Swift - 메모리 관리 / 메모리 참조 방법 (weak / strong / unowned)
[문과 코린이의 IT 기록장] IOS Swift - 메모리 관리 / 메모리 참조 방법 (weak / strong / unowned)
1. ARC (Automatic Reference Counting = 자동 참조 계수)
- IOS에서 앱의 메모리 사용을 자동으로 추적 및 관리해주는 역할을 한다.
* 코드를 작성했을 때, 해당 참조하는 인스턴스들이 더 이상 필요하지 않게 된다면 ARC가 자동으로 메모리를 해제해준다.
- 이는 클래스에서 만들어진 객체에서만 적용되며, 밸류 타입 (구조체 / Enum) 등의 객체에는 적용되지 않는다.
- 인스턴스에 대한 모든 강한 참조들이 없어진다면, ARC는 자동으로 메모리를 해제해준다.
- 즉 객체의 reference count(강한 참조 개수)에 대해 자동으로 관리해주는 역할을 한다.
ex. ARC 작동 예시 1
import Foundation
class ARCTest{
// 멤버 변수
let name : String
// 생성자
init(name : String){
self.name = name
print("생성자 완료")
}
// 소멸자
deinit{
print("소멸자 완료")
}
}
// (해석) ARCTest 객체를 만들었고, ref1이라는 참조를 통해서 사용하겠다는 의미
// 즉 이 객체는, ref1이라는 포인터를 통해서 소유하고 있다는 의미
// 이 과정에서 소유가 생겼기 때문에, reference Count가 0 -> 1로 증가하게 된다.
var ref1 : ARCTest? = ARCTest(name : "ARC") // ref1 객체 생성
// (해석) ARCTest 객체를, ref2라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 1 -> 2로 증가하게 된다.
var ref2 : ARCTest? = ref1 // ref1에 대한 강한 참조
// (해석) ARCTest 객체를, ref3라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 2 ->3로 증가하게 된다.
var ref3 : ARCTest? = ref1 // ref1에 대한 강한 참조
// (해석) 더 이상 ref1이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 3 -> 2로 감소된다.
ref1 = nil // 메모리 해제
// (해석) 더 이상 ref2이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 2 -> 1로 감소된다.
ref2 = nil // 메모리 해제
// 그러나 ref3이 아직 ref1을 강한 참조하고 있으므로, 메모리 해제에 대한 소멸자(deinit)은 실행되지 않는다.
ex. ARC 작동 예시 2
import Foundation
class ARCTest{
// 멤버 변수
let name : String
// 생성자
init(name : String){
self.name = name
print("생성자 완료")
}
// 소멸자
deinit{
print("소멸자 완료")
}
}
// (해석) ARCTest 객체를 만들었고, ref1이라는 참조를 통해서 사용하겠다는 의미
// 즉 이 객체는, ref1이라는 포인터를 통해서 소유하고 있다는 의미
// 이 과정에서 소유가 생겼기 때문에, reference Count가 0 -> 1로 증가하게 된다.
var ref1 : ARCTest? = ARCTest(name : "ARC") // ref1 객체 생성
// (해석) ARCTest 객체를, ref2라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 1 -> 2로 증가하게 된다.
var ref2 : ARCTest? = ref1 // ref1에 대한 강한 참조
// (해석) ARCTest 객체를, ref3라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 2 ->3로 증가하게 된다.
var ref3 : ARCTest? = ref1 // ref1에 대한 강한 참조
// (해석) 더 이상 ref1이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 3 -> 2로 감소된다.
ref1 = nil // 메모리 해제
// (해석) 더 이상 ref2이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 2 -> 1로 감소된다.
ref2 = nil // 메모리 해제
// (해석) reference Count가 0이 되었으므로 ARCTest 객체가 해제되게 된다.
// reference Count 1 -> 0으로 감소된다.
ref3 = nil
// ref3까지 메모리 해제하게 되면서, ref1 객체에 대한 강한 참조가 모두 사라지게 된다.
// 따라서 소멸자(deinit)가 실행되게 된다. (ARC가 자동으로 작동한다는 근거가 됨)
2. 메모리 참조 방법
- 강한 참조만 쓰게 된다면, 메모리가 낭비되지 않도록 메모리 해제를 위해 굉장히 많은 신경을 써야 할 것이다.
- 이 문제를 해결하기 위해 IOS에서는 weak / unowned를 이용할 수 있도록 하며 이를 사용하면, 조금 더 효율적으로 메모리를 관리 가능하다.
1) strong (강한 참조)
- 강한 참조는 객체를 꼭 소유해야할 때 표현하는 방식이다. (이 객체는 필요한 객체라는 것을 명시하는 것이다)
- 강한 참조를 하게 되면 reference count(강한 참조 개수)가 증가하게 된다.
* reference count가 0이라면, 객체 사용이 끝이 났다는 신호이다. => ARC를 통해 소멸자 수행
- 강한 참조는, reference count를 증가시켜, ARC로 인한 메모리 해제를 피하고, 객체를 안전하게 사용하고자 할 때 사용한다.
(1) 객체 소유 변수
- 객체를 소유하는 참조 변수의 종류에 따라, 강한 참조에 의한 객체의 메모리가 해제 되는 시점이 다르다.
a. 지역 변수 : 함수 종료와 함께 소유권 해제
class MyApplication{
func sayHello(){
var obj : MyClass!
obj = MyClass() // reference Count + 1
print("Hello")
} // 함수가 종료되면서 obj라는 지역변수가 사라진다. 그리고 이와 동시에 객체에 대한 소유권이 사라진다.
// reference Count - 1
}
b. 프로퍼티 : 객체 생명 주기에 따라 소유권 해제
class MyApplication{
var obj : MyClass! // 프로퍼티로 객체를 소유하는 방식
init(){
obj = MyClass() // myApplication 객체를 생성할 때의 생성자임
// 따라서 var obj : MyClass! = MyClass() 와 같은 의미.
// MyApplicaton이라는 객체가 존재하는 한, obj는 계속 존재하게 된다.
}
func hello(){
obj.howAreYou()
}
}
c. 타입 프로퍼티 : 해당 객체에 nil을 넣어서 소유권 해제
class MyApplication{
static var obj : MyClass!
}
MyApplication.obj = MyClass() // reference count + 1
MyApplication.obj = nil // 객체에 nil을 넣어서 소유권 해제 : reference count - 1
(2) 콜렉션과 소유권
- 콜렉션에 객체 저장시 : 콜렉션이 객체 소유
- 콜렉션에서 객체 삭제시 : 소유권 해제
- 콜렉션 객체 해제시 : 소유권 해제
var obj : MyClass! = MyClass() // obj 포인터로 MyClass 객체 가르킴. reference Count + 1
var array = [obj] // array 배열 내 원소로 MyClass에 대한 소유권 가지게 됨. reference Count + 1
obj = nil // 메모리 소유권 해제. reference Count - 1
// array 배열이 MyClass 객체에 대한 소유권을 가지고 있으므로, 아직 reference Count가 남아있음.
array.remove(at:0) // 배열의 소유권 삭제. reference Count - 1
// MyClass 객체는 메모리 상에서 해제됨.
(3) 예시
// 위의 ARC 예시와 같은 코드입니다.
import Foundation
class ARCTest{
// 멤버 변수
let name : String
// 생성자
init(name : String){
self.name = name
print("생성자 완료")
}
// 소멸자
deinit{
print("소멸자 완료")
}
}
var ref1 : ARCTest? = ARCTest(name : "ARC") // ref1 객체 생성
var ref2 : ARCTest? = ref1 // ref1에 대한 강한 참조
var ref3 : ARCTest? = ref1 // ref1에 대한 강한 참조
ref1 = nil // 메모리 해제
ref2 = nil // 메모리 해제
ref3 = nil
// ref3까지 메모리 해제하게 되면서, ref1 객체에 대한 강한 참조가 모두 사라지게 된다.
// 따라서 소멸자(deinit)가 실행되게 된다.
(4) 강한 순환 참조(Strong Reference Cycles)와 메모리 누수(Memory Leak) 문제
- 객체에 대한 소유관계가 잘못된 경우에는 메모리 누수가 일어날 수 있으며, 강한 순환 참조는 2개 이상의 관계에서도 가능하다.
- 이는 서로 소유권을 가지므로 해제되지 않는 상황이다.
- 이럴 경우에는 수동으로 해제하도록 프로그래머가 작성해야 한다.
ex. 정상 해제 상황
class ClassA{
var objB : ClassB!
deinit { print("A 객체 해제") }
}
class ClassB{
var objA : ClassA!
deinit { print("B 객체 해제") }
}
var a : ClassA! = ClassA() // ClassA객체에 대한 reference Count + 1
var b : ClassB! = ClassB() // ClassB객체에 대한 reference Count + 1
a = nil // ClassA()객체에 대한 reference Count - 1
b = nil // ClassB()객체에 대한 reference Count - 1
ex . 강한 순환 참조 발생
class ClassA{
var objB : ClassB!
deinit { print("A 객체 해제") }
}
class ClassB{
var objA : ClassA!
deinit { print("B 객체 해제") }
}
var a : ClassA! = ClassA() // ClassA객체에 대한 reference Count + 1
var b : ClassB! = ClassB() // ClassB객체에 대한 reference Count + 1
a.objB = b // ClassB객체에 대한 reference Count + 1
b.objA = a // ClassA객체에 대한 reference Count + 1
a = nil // ClassA객체에 대한 reference Count - 1
b = nil // ClassB객체에 대한 reference Count - 1
// reference Count가 각각 1개씩 남아있음.
// 서로에 대한 프로퍼티에 대해 상호 소유하고 있기 때문에 메모리 누수 발생
2) weak (약한 참조)
- 객체를 참조하지만, reference count(강한 참조 개수)는 변화하지 않는다. * 객체를 소유하지 않는 방법
- 약한 참조는 강한 순환 참조를 해결하기 위해 사용되는 보편적인 방법이다.
- 주로 인스턴스의 생명주기가 짧을 때 사용한다. * 상호 독립적으로 존재하는 객체에 사용한다
ex. 사용자와 스마트폰 / 운전자와 자동차
- 약한 참조는 순환 참조에 의한 메모리 누수 문제를 막기 위해 사용한다.
- weak는 참조하던 객체가 해제되면 nil이 된다. 따라서 nil값을 가질 수 있으므로 이는 옵셔널 타입이다.
import Foundation
class StrongRef1{
weak var strongRef : StrongRef2? = nil // 약한 참조
}
class StrongRef2{
var strongRef : StrongRef1? = nil // 약한 참조
}
var strongRef1 : StrongRef1? = StrongRef() // strongRef1 객체 변수 생성
var strongRef2 : StrongRef2? = StrongRef() // strongRef2 객체 변수 생성
strongRef1?.strongRef2 = strongRef2
strongRef2?.strongRef1 = strongRef1
// 두 객체의 메모리 해제
strongRef1 = nil
strongRef2 = nil
// 두 객체 변수는 nil을 넣어서 메모리가 해제되었다.
3) unowned (미소유 참조)
- 객체를 참조하며, reference count(강한 참조 개수)는 변화하지 않는다. * 객체를 소유하지 않는 방법
- 이 또한 약한 참조와 마찬가지로, 인스턴스가 참조하는 것을 강하게 유지하지 않는다.
- 그러나 이는 다른 인스턴스와 같은 생명주기를 가지거나 더 긴 생명주기를 가질 때 사용한다.
- 미소유 참조는, Optional는 사용하지 못한다. 따라서 nil값을 가질 수 없고 항상 값을 지니고 있어야 한다.
* 따라서 완전히 종속적인 경우에 사용한다. ex. 신용카드와 사용자 / 국가와 수도
- 이는 참조하던 객체가 해제되도 nil로 변화하지 않는다. 따라서 Dangling pointer 위험이 존재한다.
- 미소유 참조는, 객체의 생명주기가 명확하고, 개발자에 의해 제어 가능이 명확한 경우, weak Optional 타입 대신해 사용한다.
ex. 문제 발생
class Contury{
var capital : Capital!
}
class Capital{
unowned var country : Country // unowned는 옵셔널로 선언할 수 없음
init(country : Country){ // 따라서 초기화가 필요함 (생성자에서 초기화)
self.country = country
}
}
var korea : Country! = Country() // Country 객체에 대한 reference Count + 1
var seoul : Capital! = Capital(country : korea) // Capital 객체에 대한 reference Count = 1
// country에 대한 reference Count는 증가하지 않음.
korea.capital = seoul // korea의 captital 변수에 seoul 주소값 넣기
// 객체 해제 (오류 발생)
korea = nil;
ex. 정상 장동
class Contury{
var capital : Capital!
}
class Capital{
unowned var country : Country // unowned는 옵셔널로 선언할 수 없음
init(country : Country){ // 따라서 초기화가 필요함 (생성자에서 초기화)
self.country = country
}
}
var korea : Country! = Country() // Country 객체에 대한 reference Count + 1
var seoul : Capital! = Capital(country : korea) // Capital 객체에 대한 reference Count = 1
// country에 대한 reference Count는 증가하지 않음.
korea.capital = seoul // korea의 captital 변수에 seoul 주소값 넣기
// 객체 해제 확인
seoul = nil korea = nil;