ARC는 Swift의 메모리 관리기법이다.
RC 즉, Reference Counting이라는걸 이용해 자신을 참조하고 있는 수를 계산해서 메모리에서 해제할지를 결정하는 방식인데..
이번 글에서는 ARC에 대한 개념적인 내용은 생략하고, 실제 코드에서의 흐름을 조금 더 살펴볼 예정이다.
물론 자세한 코드는 뒤에서 볼 예정이지만, 흐름 중 다음과 같은 내용이 있었다.
"두 개의 클래스가 서로를 참조하고 있는 상황에서, 그 중 하나의 연결을 끊고, 해당 클래스를 참조하는 인스턴스에 nil을 할당해서 연결을 끊으면 모든 연결관계가 끊어져서 Deinit이 된다"
그런데 문득 이런 궁금증이 생겼다.
왜 서로 물려있는 연결관계중에 하나만 끊어줘도 모든 연결관계가 끊어질까?
이러한 의문은 흐름을 천천히 살펴보면서 해결해보겠다.
ARC 원리와 흐름
- ARC는 클래스의 인스턴스가 더이상 필요하지 않을 때 메모리를 자동으로 해제한다
class Guild {
var name: String
init(name: String) {
self.name = name
print("Guild Init")
}
}
class User {
var nickname: String
init(nickname: String) {
self.nickname = nickname
print("User Init")
}
}
var myGuild: Guild? = Guild(name: "최강길드") // RC + 1
var character: User? = User(nickname: "도사") // RC + 1
- 위 코드를 그림으로 나타내면 다음과 같음

- 여기서 스택영역에 있는 `myGuild`와 `character`가 할일을 다 끝내고 스택영역에서 사라지면?(코드에선 예를 들기 위해 `nil`을 할당)
myGuild = nil
character = nil

⇒ 이렇게 인스턴스들은 힙영역에 남아있게 되는가? ❌
⇒ ARC로 인해 자동으로 해제됨

⇒ 자동으로 인스턴스까지 해제된 상태
- 즉, RC가 0이되면 자동으로 메모리(힙 영역)에서 삭제가 되는것임
- 다음 코드에서는 각각의 인스턴스가 참조하고있을 뿐 아니라 각 인스턴스 안에서의 프로퍼티로 인해 서로를 참조하는 상황
class Guild {
var name: String
var owner: User?
init(name: String) {
self.name = name
print("Guild Init")
}
}
class User {
var nickname: String
var guild: Guild?
init(nickname: String) {
self.nickname = nickname
print("User Init")
}
}
var myGuild: Guild? = Guild(name: "최강길드") // RC + 1
var character: User? = User(nickname: "도사") // RC + 1
myGuild?.owner = character // RC + 1
character?.guild = myGuild // RC + 1
- 위 코드를 그림으로 나타내면 다음과 같음

- 여기서 스택영역에 있는 `character`, `myGuild`에 `nil`을 할당해서 없어지게 하면 각 클래스 인스턴스끼리 참조하고 있는 애들을 없앨 수 있는 방법이 없어지게 됨!
⇒ 각 클래스의 RC는 1인 상태로 영구적으로 존재하게 돼버림
myGuild = nil
character = nil

⇒ 강한순환참조로 인한 메모리누수 발생
- 그럼 스택영역의 `character`, `myGuild`에 `nil`을 할당하기 전에 클래스 인스턴스 안에서 서로 참조하는 애들부터 끊어준다면?
character?.guild = nil

- 일단 `character.guild = nil` 의 의미는 User 인스턴스 안의 `guild`라는 프로퍼티에는 원래 Guild 인스턴스를 참조하고 있던 `myGuild`가 있었는데, 그거 대신에 `nil`을 할당함으로써 Guild를 참조하고있던 RC가 -1이 됨
⇒ 그래서 현재 상태는 Guild의 RC는 1, User의 RC는 2인 상태
- 이 상태에서 `myGuild`, `character`에 `nil`을 할당함으로써 각각 인스턴스의 참조를 해제해준다면?
myGuild = nil
character = nil

- 스택영역에 있던 `myGuild`와 `character`는 `nil`이 되면서 스택영역에서 사라짐
- 각각 참조하고 있었던 인스턴스들의 RC가 -1씩 되면서 Guild의 RC는 0, User의 RC는 1이 됨
- 이때! Guild의 RC가 0이 된다는것의 의미는!
- Guild를 참조하고있는 애가 아무도 없다
- ARC의 동작으로 인해 아무것도 참조하고 있지 않은 인스턴스는 메모리에서 해제한다
- 즉, Guild인스턴스는 더이상 힙영역에 존재하지 않게 된다
- 그말인 즉슨 Guild 인스턴스 안의 `name`, `owner` 프로퍼티들도 존재하지 않게 된다
- `owner`에는 User 인스턴스를 참조하는 값이 들어있었는데 이친구도 사라지게 된다
- 결국 User를 참조하고 있는 RC도 -1이 돼서 User의 RC는 0이 된다. ! !

- User의 RC도 0이 됨으로써 User 인스턴스 역시 힙 영역에서 사라지게 된다
- 모든 인스턴스들이 사라지면서 메모리에 아무것도 남아있지 않게 된다

그래서 서로 참조하는 애들중에 하나만 nil을 해줘도 전체가 메모리에서 해제가 되는 것이다!
weak의 역할
- 이렇게 참조하는 값들을 하나하나 찾아서 `nil`을 할당해주는것은 현실적으로 불가능 ⇒ 참조가 일어날것같은 애들한테 `weak` 혹은 `unowned` 키워드를 붙여준다
- `weak` 키워드가 붙은 애들이 다른 클래스를 참조할 때 RC가 올라가지 않게 된다
- `weak` 키워드가 붙은 애들은 반드시 옵셔널 타입이어야 한다
class Guild {
var name: String
weak var owner: User?
init(name: String) {
self.name = name
print("Guild Init")
}
}
class User {
var nickname: String
var guild: Guild?
init(nickname: String) {
self.nickname = nickname
print("User Init")
}
}
var myGuild: Guild? = Guild(name: "최강길드") // RC + 1
var character: User? = User(nickname: "도사") // RC + 1
myGuild?.owner = character // RC 변동 X
character?.guild = myGuild // RC + 1

- `owner`가 `weak`으로 선언되어있어서 `chracter`를 통해 User 인스턴스를 참조하고 있더라도 RC가 올라가지 않음
- 이 상태에서 스택 영역에 있는 `myGuild`와 `character` 변수들에 `nil`을 할당해서 없앤다면?
myGuild = nil
character = nil
⇒ 각 인스턴스들의 RC가 -1씩 될것임

- User 인스턴스의 RC가 0이 되었다!!
- User를 참조하고있는 애가 아무도 없다
- ARC의 동작으로 인해 아무것도 참조하고 있지 않은 인스턴스는 메모리에서 해제한다
- 즉, User인스턴스는 더이상 힙영역에 존재하지 않게 된다
- 그말인 즉슨 User 인스턴스 안의 `nickname`, `guild` 프로퍼티들도 존재하지 않게 된다
- `guild`에는 Guild 인스턴스를 참조하는 값이 들어있었는데 이친구도 사라지게 된다
- 결국 Guild를 참조하고 있는 RC도 -1이 돼서 Guild의 RC는 0이 된다.
- Guild의 RC도 0이 됨으로써 Guild 인스턴스 역시 힙 영역에서 사라지게 된다

- 모든 인스턴스들이 사라지면서 메모리에 아무것도 남아있지 않게 된다

마찬가지로 서로 참조하는 애들중에 하나에만 weak을 해줘도 전체가 해제되는 것이다..!!
weak과 unowned의 차이는?
- 현재 `owner`에는 `weak` 키워드가 붙어있고, `owner`의 값인 `character`가 참조하는 User 인스턴스가 사라진 상태
- 이렇게 되면 `weak` 키워드가 붙은 `owner`에는 자동으로 `nil`이 할당되게 됨

- 반면 unowned는?
- `owner`에 `weak`이 아닌 `unowned`키워드가 붙는다면 참조하는 값이 없어지면 자동으로 nil을 할당해주지 않고 계속 값을 찾게됨
class Guild {
var name: String
unowned var owner: User?
// ...
}

⇒ 이렇게 되면 앱 동작에 문제가 생길 수도 있음

- 일반적으로 weak과 unowned를 구분하는 경우
- 보통 수명이 더 짧은 인스턴스를 가리키는 애를 `weak` 해줌
- 예를 들어 Guild가 사라지는것보다 User가 탈퇴할 가능성이 더 높다 User 인스턴스를 참조하는 Guild의 `owner`가 `nil`이 될 가능성이 더 높다 그럼 Guild의 `owner`를 `weak`으로 선언한다
- 반면, 수명이 더 긴 인스턴스를 가리키는 애를 `unowned`해줌
- 예를 들어 Guild가 사라지는것보다 User가 탈퇴할 가능성이 더 높다 그렇다면 만약 Guild의 `owner`에 `unowned`를 할 경우 오류가 날 가능성이 생긴다 그래서 수명이 더 긴 Guild인스턴스를 가리키는 User의 `guild`를 `unowned`해줌
※참고자료
https://babbab2.tistory.com/26
iOS) 메모리 관리 (1/3) - ARC(Automatic Reference Counting)
안녕하세요~~ 소들입니다 👀 오늘은 지난 시간 메모리 구조에 이어 Swift를 사용할 때 메모리 관리가 어떤 식으로 되는지에 대해 공부해볼 거예요 :) ARC 면접 단골 질문이라죠? 깔깔 iOS 개발자라
babbab2.tistory.com
'iOS > Swift' 카테고리의 다른 글
| [iOS/Swift] Typed throws - Swfit6에서의 에러처리 패턴 개선사항 (0) | 2025.02.16 |
|---|---|
| [iOS/Swift] MVVM의 한계에 대한 고찰 - Massive View Model 해결법 (0) | 2025.02.11 |
| [iOS/Swift] 커스텀 Observable와 bind메서드에 대한 고찰 (0) | 2025.02.04 |
| [iOS/Swift] WMO와 Method Dispatch - class에 final을 붙이는 진짜 이유 (0) | 2025.01.25 |
| [iOS/Swift] GCD 골든벨 - 다양한 상황에서의 동기/비동기처리 출력결과 예측하기 (0) | 2025.01.22 |