Strong
- 강한 참조
- 인스턴스의 주소값이 변수에 할당될 때 RC가 증가하는 경우
- 평소에 내가 인스턴스 생성하고 사용하던 것이 다 강한 참조를 한 것이다. (기본값이 strong이기 때문이다.)
class Developer {
var name: String?
var language: String?
init(name: String?, language: String?) {
self.name = name
self.language = language
}
}
let r1verfuture = Developer(name: "r1verfuture", language: "Swift")
- [코드 1], [코드 2] 를 실행하면 강한 참조를 한 것이다.
Circular Reference
- 순환 참조
- ARC 의 단점
- 영구적으로 메모리가 해제되지 않을 수 있다.
class Man {
var name: String
var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man Deinit") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman Deinit") }
}
- [코드 3] 에는 Man 클래스에는 Woman 타입의 프로퍼티가 있고, Woman 클래스에는 Man 타입의 프로퍼티가 있다.
var whatso: Man? = .init(name: "왓소")
var r1verfuture: Woman? = .init(name: "밀애")
- [코드 3], [코드 4] 를 실행하면 [코드 4] 의 첫번째 줄에서 Man 인스턴스의 RC값이 1이 된다. (강한 참조)
- [코드 4] 의 두번째 줄에서 Woman 인스턴스의 RC값이 1이 된다. (강한 참조)
whatso?.girlfriend = r1verfuture
r1verfuture?.boyfriend = whatso
- [코드 3], [코드 4], [코드 5] 를 실행하면 [코드 4] 에 의해 Man 인스턴스와 Woman 인스턴스의 RC값이 각각 1씩 증가된다. (↑ 바로 위 설명 참고)
- [코드 5] 의 첫번째 줄에서 Woman 인스턴스의 RC값이 1 증가된다. (즉, Woman 인스턴스의 RC값이 2가 된다.)
- [코드 5] 의 두번째 줄에서 Man 인스턴스의 RC값이 1 증가된다. (즉, Man 인스턴스의 RC값이 2가 된다.)
- 이렇게 2개의 객체가 서로를 참조하고 있는 형태를 순환 참조라고 하며, 순환 참조 중에서 강한 참조로 인해 순환 참조가 발생한 경우를 강한 순환 참조라고 한다.
- weak 와 unowned 사용하면 강한 순환 참조를 해결할 수 있다.
whatso = nil
r1verfuture = nil
- [코드 3], [코드 4], [코드 5], [코드 6] 을 실행하면 인스턴스를 가리키던 변수에 nil을 대입했으니까 RC값이 1 감소하고, 감소한 결과로 RC값이 0이 되면 메모리에서 해제될 것 같지만 그렇지 않다. (실행해보면 deinit 함수가 호출되지 않는 것을 볼 수 있다.)
- whatso, r1verfuture 가 가리키던 인스턴스가 힙에서 사라지지 않고 계속 메모리를 먹고 있는 것이다. (Man과 Woman 인스턴스의 RC값은 각각 1로 남아있다.)
- 심지어 [코드 6] 처럼 nil을 부여해버리면 Man과 Woman 인스턴스에 접근할 방법도 없어서 메모리 해제도 못하고, 앱이 죽기 전까지 기다려야 한다. (메모리 누수가 생기는 것이다.)
Weak
- 약한 참조
- 인스턴스를 참조할 때 RC값을 증가시키지 않는다.
- 참조하던 인스턴스가 메모리에서 해제된 경우, 자동으로 nil이 할당되어 메모리가 해제된다.
- 프로퍼티를 선언한 이후, 나중에 nil이 할당되는 것이기 때문에 weak는 무조건 옵서널 타입의 변수여야 한다.
- 강한 순환 참조는 2개의 객체가 서로를 참조하고 있는 형태라고 했는데, 이 2개의 객체 중 하나를 weak로 선언하면 순환 참조에서 벗어날 수 있다. (2개의 객체가 수명이 같은 경우에는 아무 쪽에나 weak를 붙여줘도 상관없지만, 수명이 다른 경우에는 둘 중에 수명이 더 짧은 인스턴스를 가리키는 애를 약한 참조로 선언해야 한다.)
- 순환 참조를 일으키는 프로퍼티 앞에 weak를 붙여주면 된다.
class Man {
var name: String
weak var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man Deinit") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman Deinit") }
}
- [코드 4], [코드 5], [코드 6], [코드 7] 을 실행하면 [코드 3] 에서 발생할 수 있던 순환 참조를 해결할 수 있다.
- [코드 4] 에서 Man과 Woman 인스턴스의 RC값이 각각 1 이 된다.
- [코드 5] 의 첫번째 줄에서 'girlfriend' 프로퍼티가 weak이기 때문에 Woman 인스턴스의 주소값을 할당 (참조) 받기는 하지만 Woman 인스턴스의 RC값은 증가하지 않는다. (Woman 을 참조했는데도 불구하고 Woman 의 RC값은 그대로 1 이다.)
- [코드 5] 의 두번째 줄에서 Man 인스턴스의 RC값이 2 로 증가한다. (weak 아니기 때문이다.)
- [코드 6] 의 첫번째 줄에서 whatso 에 nil 이 부여되었기 때문에 2 였던 Man 인스턴스의 RC값이 1 줄어들어 1 이 된다. (whatso 가 사라지면서 girlfriend 프로퍼티의 RC값도 덩달아 1 줄어들어 0 이 되고, girlfriend 프로퍼티가 메모리에서 해제된다.)
- [코드 6] 의 두번째 줄에서 r1verfuture 에 nil 이 부여되었기 때문에 1 이었던 Woman 인스턴스의 RC값이 1 줄어들어 0 이 되고, Woman 인스턴스가 메모리에서 해제된다. (r1verfuture 가 사라지면서 1 이었던 boyfriend 프로퍼티의 RC값도 덩달아 1 줄어들어 0 이 되고, 메모리에서 해제된다.)
- 이대로 실행하면 deinit 함수도 정상 작동한다.
- 이렇게 weak 를 써서 강한 순환 참조를 해결하는 것을 약한 순환 참조라고 한다.
Unowned
- 미소유 참조
- 강한 순환 참조를 해결할 수 있고, RC값을 증가시키지 않는다는 점에서 weak 와 같지만, 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하는 점이 weak 와 다르다.
- 참조하던 인스턴스가 메모리에서 해제된 경우 nil 을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있는다.
- unowned 로 선언된 변수가 가리키던 인스턴스가 메모리에서 먼저 해제된 경우, 접근하려고 하면 에러가 발생한다.
- 가리키던 메모리가 해제되어도 nil 을 할당받지 않아서 Non-Optional Type 으로 선언해야 됐지만 Swift 5.0 부터는 옵셔널 타입으로 선언 가능하도록 바뀌었다.
class Man {
var name: String
unowned var girlfriend: Woman?
init(name: String) {
self.name = name
}
deinit { print("Man Deinit") }
}
class Woman {
var name: String
var boyfriend: Man?
init(name: String) {
self.name = name
}
deinit { print("Woman Deinit") }
}
- [코드 8] 에서 'unowned' 로 선언된 whatso 의 girlfriend 프로퍼티가 가리키는 r1verfuture 인스턴스는 whatso 인스턴스가 메모리에서 해제되기 전까지는 절대 먼저 해제되어서는 안된다.
r1verfuture = nil
- [코드 7], [코드 9] 를 실행하면 Man 의 girlfriend 프로퍼티를 weak 로 해줬기 때문에 자동으로 girlfriend 값이 nil 로 지정된다.
- 하지만, [코드 8], [코드 9] 를 실행하면 girlfriend 값이 nil 로 지정되지 않기 때문에 이미 해제되었는데도 불구하고 메모리 주소값을 계속 들고 있는다.
whatso?.girlfriend
- [코드 8], [코드 9], [코드 10] 을 실행하면 이미 메모리에서 해제된 포인터 값에 접근하려고 하는 것이기 때문에 에러가 발생한다.
그렇다면 Weak 와 Unowned 중 어떤 것을 쓰는게 더 좋을까 ?
- unowned 는 에러를 발생시킬 위험이 있기 때문에 weak 를 사용하는 것이 더 좋다.
- 하지만 unowned 을 굳이 써야겠다면 weak 와 반대로 2개의 객체 중 수명이 더 긴 인스턴스를 가리키는 애를 미소유 참조로 선언하면 된다. (수명이 더 긴 인스턴스가 누구일지 판단 불가능할 때 unowned 를 쓰면 에러나기 때문에 웬만하면 쓰지 않는 것이 좋다.)
참고