강한참조 (Strong)
인스턴스가 계속해서 메모리에 남아있어야 할 명분을 만들어 주는 것
인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되는데, 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 강한참조를 사용하면 참조 횟수가 1 증가한다.
또한 강한참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당해주면 참조 횟수가 1 감소한다.
참조의 기본은 강한참조이므로 클래스 타입의 프로퍼티, 변수, 상수 등을 선언할 때 별도의 식별자를 명시하지 않으면 강한참조이다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var ref1: Person?
var ref2: Person?
var ref3: Person?
ref1 = Person(name: "domisol") // 참조 횟수 1
// "domisol is being initialized"
ref2 = ref1 // 참조 횟수 2
ref3 = ref1 // 참조 횟수 3
ref3 = nil // 참조 횟수 2
ref2 = nil // 참조 횟수 1
ref1 = nil // 참조 횟수 0
print(ref1) // "nil"
// "domisol is being deinitialized"
ref1 에 할당된 Person 클래스 타입의 인스턴스는 처음 메모리에 생성된 후 강한참조로 할당되기 때문에 참조 횟수가 1 증가한다.
그 후 2, 3에도 할당되며 참조 횟수가 1씩 증가한다. 하나의 인스턴스가 세 개의 변수에 강한참조로 참조하고 있는 것이므로, 계속 메모리에 살아있을 명분이 충분함!
참조를 그만 둘 때마다 1씩 감소한다. 참조 횟수가 0이 되는 순간 인스턴스는 ARC의 규칙에 의해 메모리에서 해제되며, 메모리에서 해제되기 직전에 디이니셜라이저를 호출한다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
func foo() {
let domisol: Person = Person(name: "domisol") // 참조 횟수 1
// "domisol is ~ "
// 함수 종료 시점
// 참조 횟수 0 -> "~~ deinit"
}
foo()
foo() 라는 함수 내부에 domisol 이라 정의한 강한참조 상수가 있다.
Person 타입의 인스턴스는 이니셜라이저에 의해 생성된 후 domisol 상수에 할당할 때 참조 횟수가 1이 된다.
그리고 강한참조 지역변수(상수)가 사용된 범위의 코드 실행이 종료되면 그 지역변수(상수)가 참조하던 인스턴스의 참조 횟수가 1 감소한다.
그래서 함수가 종료되며 참조 횟수가 1 감소하여 0이 된다. 그 순간 인스턴스는 메모리에서 해제된다.
var globalRef: Person? // 전역변수 선언
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
func foo() {
let domisol: Person = Person(name: "domisol") // 참조 횟수 1
// "domisol is ~ "
globalRef = domisol // 참조 횟수 2
// 함수 종료 시점
// 참조 횟수 1 -> 메모리에서 해제되지 않음
}
foo()
인스턴스가 강한참조를 하는 전역변수 globalRef 에 강한참조되면서 참조 횟수가 1 증가하여 2가 된다.
그 상태에서는 함수가 종료되며 1 감소되어도 여전히 참조 횟수가 1이므로 메모리에서 해제되지 않는다.
강한참조 순환 문제
인스턴스끼리 서로가 서로를 강한참조 할 때, 강한참조의 규칙을 모르고 사용하게 되면 문제가 발생할 수 있다.
class Person {
let name: String
init(name: String) {
self.name = name
}
var room: Room?
deinit {
print("\(name) is being deinitialized")
}
}
class Room {
let num: String
init(num: String) {
self.num = num
}
var host: Person?
deinit {
print("Room \(num) is being deinitialized")
}
}
var domisol: Person? = Person(name: "domisol") // Person 참조 횟수 1
var room: Room? = Room(num: "606") // Room 참조 횟수 1
room?.host = domisol // Person 참조 횟수 2
domisol?.room = room // Room 참조 횟수 2
domisol = nil // Person 참조 횟수 1
room = nil // Room 참조 횟수 1
이 코드를 실행하면 디이니셜라이저가 호출되지 않는다.
최종적으로 domisol과 room에 nil을 할당해주고 나면, 각 인스턴스의 참조 횟수는 1이 된다.
하지만 이제 domisol이 참조하던 Person 클래스의 인스턴스에 접근할 방법도, room이 참조하던 Room 클래스의 인스턴스에 접근할 방법도 없어졌다. ㅜㅜ
참조 횟수가 0이 되지 않는 한 ARC의 규칙대로라면 인스턴스를 메모리에서 해제시키지 않기 때문에 이렇게 두 인스턴스 모두 참조 횟수 1을 남겨둔 채 메모리에 남아 있게 된다. ➡️ 메모리 누수
이렇게 두 인스턴스가 서로를 참조하는 상황에서 강한참조 순환 문제가 발생할 수 있다.
var domisol: Person? = Person(name: "domisol") // Person 참조 횟수 1
var room: Room? = Room(num: "606") // Room 참조 횟수 1
room?.host = domisol // Person 참조 횟수 2
domisol?.room = room // Room 참조 횟수 2
room?.host = nil // Person 참조 횟수 1
domisol?.room = nil // Room 참조 횟수 1
domisol = nil // Person 참조 횟수 0
// " ~~ deinit "
room = nil // Room 참조 횟수 0
// " ~~ deinit "
이렇게 하나씩 변수 또는 프로퍼티에 nil 을 할당하면, 참조 횟수가 감소하기 때문에 인스턴스를 메모리에서 해제시킬 수 있다.
하지만 실수로 이 코드를 빼먹거나, 해제해야 할 프로퍼티가 너무 많다면?
약한참조(weak) 또는 미소유참조(unowned) 를 사용하면 됩니다~
(unowned는 swift 5.0 이후로 변경사항이 있기 때문에 일단 weak 위주로 공부해보기 .. )
참고 : 한빛미디어 swift 프로그래밍 2판
'swift' 카테고리의 다른 글
[swift] 구조체 (struct) (0) | 2020.06.22 |
---|---|
[swift] ARC | 약한참조 (weak) (0) | 2020.06.11 |
[swift] ARC란? (0) | 2020.06.11 |
[swift] 데이터 타입 | Any, AnyObject (0) | 2020.06.08 |
[swift] 스위프트의 언어적 특성 2. 함수형 (0) | 2020.06.08 |