Swift를 처음 접하면 반드시 마주치는 개념, 바로 ARC(Automatic Reference Counting)
저는 처음에 아 이건 고급용어니까 나중에 더 잘해졌을 때 공부해야지! 지금 옵셔널도 헷갈리는데 뭔 ARC냐.. 했는데, 전혀 어렵지 않은 친구였습니다..!
일단 풀어서 설명하면 그냥 자동 참조 카운팅인거라서, 이게 메모리를 관리해주는 기법이다! 정도만 이해하시면 될 것 같네요!
이 글에서는 간단하게나마 이제 ARC를 파악하고 갈 수 있게 한 번 짜보도록 하겠습니다.
Automatic Reference Counting - 말 그대로, Swift가 객체의 메모리 관리를 자동으로 해주는 시스템입니다. swift가 알아서 객체의 ‘참조 횟수’를 세고, 필요 없어지면 메모리에서 제거합니다.
둘 다 메모리를 자동으로 관리해 주는 건 맞지만, 방식이 좀 다르다.
즉, 목적은 비슷하지만 언제(컴파일 vs 런타임)와 어떻게(정적 분석 vs 동적 검사) 작동하는지가 근본적으로 다르다.
앱에서 객체들이 계속 만들어지고 사라지는데, 안 쓰는 객체가 메모리에 계속 남아있으면 앱이 느려지거나 꺼지는데, Swift는 이걸 자동화해서 안쓰면 바로바로 치워주는 야무진 친구를 만든거다!
--
이름에서 봤듯이 레퍼런스 카운트 - 참조 카운트가 핵심이다.
엄청 간단하다 !
let obj1 = MyClass() // 참조 카운트: 1 (obj1이 참조)
let obj2 = obj1 // 참조 카운트: 2 (obj1, obj2가 참조)
// obj1 = nil // 참조 카운트: 1 (obj2만 참조)
// obj2 = nil // 참조 카운트: 0 → 메모리 해제!
이렇게만 보면 정말 간단하고 문제될게 없을 것 같은데, 야무진 친구가 일을 잘해주는 대신에 허점이 있다..! 그 중 하나가 순환참조라는 것이다.
근데 이 순환참조에 대해서 설명하기 전에 참조의 종류에 대해서 알아볼 필요가 있다..
Swift에서는 참조하는 방식이 크게 3가지가 있다.
1. Strong Reference (강한 참조)
2. Weak Reference (약한 참조)
weak
키워드 사용3. Unowned Reference (미소유 참조)
unowned
키워드 사용class Person {
let name: String
weak var bestFriend: Person? // 약한 참조
unowned let birthPlace: City // 미소유 참조
init(name: String, birthPlace: City) {
self.name = name
self.birthPlace = birthPlace
}
}
그래서 이제 강한참조를 알게 되셨으니!! 순환참조 오류에 대해서 설명을 해보겠습니다.
class Person {
var friend: Person?
}
var a: Person? = Person()
var b: Person? = Person()
a?.friend = b
b?.friend = a
여기서 뭔 일이 일어났는지 차근차근 보자.
a
라는 변수가 Person 인스턴스를 참조 -> 참조 카운트: 1b
라는 변수가 또 다른 Person 인스턴스를 참조 -> 참조 카운트: 1 a?.friend = b
-> b의 참조 카운트: 2 (b변수 + a의 friend 프로퍼티)b?.friend = a
-> a의 참조 카운트: 2 (a변수 + b의 friend 프로퍼티)이제 문제가 생긴다.
a = nil // a 변수는 사라졌는데, b의 friend는 여전히 a를 물고있다.
b = nil // b도 마찬가지, b가 죽었는데, a의 친구로서 남아있다..
이렇게 두 인스턴스 모두 참조 카운트가 1로 남아서 영원히 메모리에 갇혀있게 된다. 이게 바로 순환참조다.
서로가 서로를 꽉 잡고 있어서 ARC가 "어? 이거 아직 쓰고 있네?"라고 착각하는 거다. 마치 두 사람이 서로의 손을 잡고 "너 먼저 놔" "아니야 너 먼저 놔" 하면서 영원히 못 헤어지는 상황 같은 거다.
물론 영화 코코에서 봤듯이 이러면 죽어서도 영원히 이승에 남아있을 수 있긴하겠다만... Swift가 원하는 방향은 아닌것같다.
먼저 일단은 둘 다 동일하게 Person
클래스의 인스턴스다 보니까 크게 와닿지 않는 것 같아서.. 좀 더 와닿는 시나리오를 넣은 다른 예시를 들어보자!
class Dog {
var name: String
var owner: Person? // 강한 참조
init(_ name: String) {
self.name = name
}
deinit {
print("\(name) 강아지 해제됨")
}
}
class Person {
var name: String
var dog: Dog? // 강한 참조
init(_ name: String) {
self.name = name
}
deinit {
print("\(name) 해제됨")
}
}
var 철수: Person? = Person("철수")
var 멍멍이: Dog? = Dog("멍멍이")
철수?.dog = 멍멍이 // 철수 → 멍멍이 (강한 참조)
멍멍이?.owner = 철수 // 멍멍이 → 철수 (강한 참조)
철수 = nil // 철수는 죽음을 맞이했고,
멍멍이 = nil // 멍멍이도 죽음을 맞이했지만...
// 실제로는 둘 다 메모리에 아직 살아 숨쉬는중!!
// (deinit 호출 안됨 = 메모리 누수 발생)
순환 참조(Retain Cycle) 문제
이게 순환참조다. 철수는 멍멍이를, 멍멍이는 철수를 놓지 않고 있어서 둘 다 메모리에서 해제되지 않는다.
철수 객체 <-> 멍멍이 객체
| |
변수는 nil 변수는 nil
객체들만 서로를 붙잡고 떠다니는 유령 상태다... ㄷㄷ
// 저승사자가 돼서 둘의 인연을 끊어준다
철수?.dog = nil // 또는 멍멍이?.owner = nil
// 기존 코드
철수 = nil // 이제 철수가 평안히 떠난다
멍멍이 = nil // 멍멍이도 평안히 떠난다
// "철수 해제됨"
// "멍멍이 강아지 해제됨"
1. 철수?.dog = nil 실행
멍멍이 객체의 참조 카운트: 2 -> 1
2. 철수 = nil 실행
철수 객체의 참조 카운트: 2 -> 1
3. 멍멍이 = nil 실행
멍멍이 객체의 참조 카운트: 1 -> 0 (해제!)
연쇄적으로 철수 객체도 해제!
이렇게 철수를 nil로 만들기전에 철수.dog의 참조를 풀어주면
해제를 할 수가 있다.
하지만 인간은 실수의 동물이지않는가? 이런거 한줄이라도 빼먹는 순간 나도 모르는 사이에 메모리 누수가 발생할 것이다..!
특히 앱에서는 다음과 같이 사소한 코드에서도 발생할 수 있다.
// 만약 유저 관련된 프로퍼티 관계를 설정할 때
user.profile = profile
profile.owner = user
//... 여러 로직들을 지나고.. 하단에
user = nil
profile = nil
// 어? 관계 정리를 깜빡했네? -> 즉 시 메모리 누수!
그래서 이런 고민에서 등장한게 참조 카운트를 늘리지 않는 weak
과 unowned
키워드다!
weak
과 unowned
키워드이런 고민에서 등장한 것이 참조 카운트를 늘리지 않는 weak
과 unowned
키워드다!
class Dog {
var name: String
weak var owner: Person? // 약한 참조로 변경!
init(_ name: String) {
self.name = name
}
deinit {
print("\(name) 강아지 해제됨")
}
}
// 이제 순환 참조가 없음 약한 참조기 때문 !
var 철수: Person? = Person("철수")
var 멍멍이: Dog? = Dog("멍멍이")
철수?.dog = 멍멍이 // 강한 참조
멍멍이?.owner = 철수 // 약한 참조 (RC 증가 안함)
철수 = nil // 철수 해제됨 -> 멍멍이.owner 자동으로 nil
멍멍이 = nil // 멍멍이도 해제됨
// "철수 해제됨"
// "멍멍이 강아지 해제됨"
강한 참조: 나는 이 객체를 절대 놓지 않겠다! -> 참조 카운트 +1
약한 참조: 있으면 쓰고, 없으면 말고~ -> 참조 카운트 그대로
weak
/unowned
사용 (추천되는 방향)weak
을 쓰면 됩니까!- 부모 <-> 자식 관계: 자식 -> 부모 방향을 weak
- 소유자 <-> 델리게이트: 델리게이트를 weak
- 순환 참조가 의심되는 모든 곳
어느 쪽을 weak
로 만드느냐에 따라 누가 누구를 소유하는지가 결정된다.
이렇게 소유 관계를 명확히 구분해서 한쪽은 강한 참조, 다른 쪽은 약한 참조로 만들어주면 순환참조를 해결할 수 있다.
unowned에 대해서 설명을 안했다!
weak
과 함께 순환 참조를 해결하는 또 다른 키워드가 바로unowned
다.
weak과 unowned의 차이점이라.. 쉽게 설명하면 옵셔널이랑 강제언래핑이랑 비슷하다!!
이렇게 말하면 느낌이 오실 수도 있을 것 같은데, 얘도 참조카운트를 늘리지는 않지만 만약 참조하던애가 nil이되면 그냥 그대로 프로그램을 폭파시켜버린다.(크래시)
더 제대로 설명을 하자면..
weak
vs unowned
차이점weak var owner: Person? // Optional - 안전하지만 nil 체크 필요
unowned var owner: Person // Non-optional - 편하지만 위험할 수 있음
import Foundation
// Person 클래스를 정의해야 함
class Person {
var name: String
var dog: Dog?
init(_ name: String) {
self.name = name
}
deinit {
print("\(name) 해제됨")
}
}
class Dog {
var name: String
unowned var owner: Person // unowned 사용! 일단 옵셔널도 아닌걸보니 벌써부터 심상치않다..
init(_ name: String, owner: Person) {
self.name = name
self.owner = owner
}
func greet() {
print("\(owner.name)님 안녕하세요!")
}
deinit {
print("\(name) 강아지 해제됨")
}
}
var 철수: Person? = .init("철수")
var 멍멍이: Dog? = Dog("멍멍이", owner: 철수!)
철수?.dog = 멍멍이
멍멍이?.greet() // "철수님 안녕하세요!"
철수 = nil // 철수 해제됨
멍멍이?.greet() // 펑! 크래시! (해제된 객체 접근)
//출력 결과
철수님 안녕하세요!
철수 해제됨
Fatal error: Attempted to read an unowned reference but object 0x1003bba90 was already deallocated
에러메세지를 보면
소유되지 않은 참조를 읽으려고 했지만 개체 0x1003bba90이 이미 할당 해제되었습니다.
이 에러 메시지를 통해 weak와 unowned의 근본적인 차이를 알 수 있다.
그니까 이걸로 하나 알 수 있는 것은 weak는 nil값이 된걸보면 알아서 할당을 해제하지만, unowned는 포인터처럼 힙주소를 가리키던 손가락을 거두지않고, 그냥 그대로 굳어버리게 되는데, 그때 이미 할당이 해제된 주소값에 접근하니까 터지는것이다.
weak var owner: Person?
unowned var owner: Person
현실적인 조언: 99% 경우에는 weak
를 쓰자
메모리 누수는 앱이 느려질 뿐이지만, 크래시는 앱이 아예 꺼져버린다. 그래서 웬만하면 weak
가 안전하다.
철저한 종속 관계에서만 사용하자!
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer // 고객 없는 카드는 존재 불가!
}
100% 확신이 있을 때만 unowned
조금이라도 의심스러우면 weak
결론
절대적 종속 관계가 확실할 때만
unowned
, 나머지는 모두weak
을 사용해주자!
weak - 안전하지만 Optional이라 nil 체크 필요
unowned - 편하지만 잘못 쓰면 크래시
결론: 순환 참조 해결이 목적이라면 대부분 weak
을 사용하는 것이 안전하다. unowned
는 성능이 중요하고 생명주기가 확실한 특별한 경우에만 사용하자!
weak
/unowned
로 해결다음엔 또 weak 공부하다가 델리게이트 패턴에 대해서도 좀 살펴봤는데 그 부분에 대해서 포스팅을 해보면 좋을 것 같네요!!
참고