Swift ARC

peppermint100
PEPPERMINT100
Published in
8 min readOct 20, 2023

ARC는 Auto Referecing Count의 약자로 참조 개수를 자동으로 관리해줌을 의미한다.
C++, Obj-C와 같이 개발자가 직접 메모리를 관리해야 하는 언어가 있는 반면 Java, Python, Swift와 같은 고수준 언어는 메모리를 프로그래밍 언어가 직접 관리해주는 경우가 많고 Swift에서는 ARC가 그런 역할을 하게 된다.

일반적으로 메모리는 code, data, heap, stack의 구조로 이루어져 있는데,

code 영역에는 프로그램의 코드가 저장이 되고, 프로그램의 시작과 끝까지 계속 메모리에 남아 있는다.

data 영역에는 전역변수, 정적변수, 상수 등이 저장되며 이 역시 프로그램이 종료할때 까지 남아 있는다.

stack 영역에는 임시 메모리 영역으로 함수 호출과 관련된 지역변수, 매개 변수가 저장된다. 함수가 호출 될때 할당되며 호출이 완료되면 소멸한다.

heap 영역은 개발자가 직접 관리하게 되며 메모리 공간이 동적으로 할당되고 해제된다. 클래스의 인스턴스 생성, 클로저 등을 사용할 때 heap 영역이 사용된다.

중요한 부분은 개발자가 관리해야만 하는 heap 영역이며 제대로 관리하지 못할 경우 memory leak 등의 문제가 발생할 수 있다.

ARC의 메모리 관리방식

기본적으로 어떤 객체가 생성되어 메모리가 할당되면 해당 객체에 Reference Count(이하 RC, 카운트)를 1 증가시킨다. 이 숫자가 양수로 존재하면 메모리를 해제하지 않고 0이 되면 메모리를 해제한다.

Swift 이전의 Obj-C에서는 메모리를 직접 관리해주어야 했는데, heap 영역을 사용할 때마다
retain, release 의 함수를 직접 작성을 해주어야 했다.

retain은 RC를 1 증가시키고
release는 RC를 1 감소시킨다.

하지만 ARC의 등장 이후로 Obj-C, Swift에서는 자동으로 컴파일시에 retain, release코드를 알아서 삽입해준다.

ARC 확인하기

class User {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
print("name: \(name), age: \(age) User init")
}
deinit {
print("name: \(self.name), age: \(self.age) User deinit")
}
}
var aUser: User?
var bUser: User?
aUser = User(name: "A", age: 25) // count 1
bUser = aUser // count 2
aUser = nil // count 1
// bUser에 아직 User(name: "A", age: 25)가 묶여 있음 deint 되지 않음
bUser = nil //count 0
// deinit
name: A, age: 25 User init
name: A, age: 25 User deinit
  1. User 객체를 생성해서 aUser에 해당하면 RC가 1증가하며 메모리가 할당된다.
  2. name: A, age: 25 User init 이 출력된다.
  3. bUser도 aUser에 할당하면 RC가 1 증가하여 총 카운트가 2가 된다.
  4. aUser를 nil에 할당하며 RC를 1줄인다. 하지만 bUser에 할당되어 있어서 deinit 되지는 않는다.
  5. bUser도 nil로 해제하면 그 때 메모리가 해제되며 name: A, age: 25 User deinit이 출력된다.

강한 참조 순환

ARC가 메모리를 관리하지 못하게 되고 누수가 발생하게 되는 순간이 있는데, 가장 큰 이유는 강한 참조 순환이다.

어떤 객체가 다른 객체를 참조하며 RC를 1 올리고 그 다른 객체도 어떤 객체를 참조하여 RC가 둘다 2가 되는 순간이다.

이렇게 되면 두 객체가 모두 nil이 할당되더라도 RC가 1씩 남기 때문에 메모리가 해제되지 않는다. 아래와 같은 경우이다.

class User {
var name: String
var bestFriend: User?
init(name: String) {
self.name = name
print("name: \(name)User init")
}
deinit {
print("name: \(self.name) User deinit")
}
}
var pepper: User?
var mint: User?
pepper = User(name: "pepper") // count 1 for pepper
mint = User(name: "mint") // count 1 for mint
pepper?.bestFriend = mint // count 2 for.pepper
mint?.bestFriend = pepper // count 2 for.mint
pepper = nil // count 1 for pepper
mint = nil // count 1 for mint

물론 pepper?.bestFriend = nil 이 후에 pepper 객체를 해제해주면 해결이 가능하나 모든 경우에서 이를 개발자가 신경쓰기란 쉽지 않다.

약한 참조

이런 경우엔 약한 참조를 통해 문제를 해결할 수 있다.

약한 참조는 weak 키워드를 통해 만들 수 있으며 해당 프로퍼티에 객체를 할당하는 것으로 RC를 올리지 않게 한다.

class Owner {
var name: String
weak var pet: Pet?

init(name: String) {
self.name = name
print("name: \(name) Owner init")
}
deinit {
print("name: \(self.name) Owner deinit")
}
}
class Pet {
var name: String
var owner: Owner?
init(name: String) {
self.name = name
print("name: \(name)Pet init")
}
deinit {
print("name: \(self.name) Pet deinit")
}
}
var pepper: Owner?
var mint: Pet?
pepper = Owner(name: "pepper") // count 1 for pepper
mint = Pet(name: "mint") // count 1 for mint
pepper?.pet = mint // count 1 for Pet mint >> Pet이 weak이기 때문에 mint의 카운트를 올리지 않음
mint?.owner = pepper // count 2 for Owner pepper
mint = nil // mint가 0이 되며 mint 해제 또 mint의 owner여서 올라갔던 pepper의 RC도 2 -> 1
pepper = nil // pepper의 카운트가 1->0이되며 해제

주인인 pepper에 mint라는 Pet을 할당해도 카운트를 올리지 않는다.
그래서 mint를 해제함으로서 바로 mint의 카운트가 1->0이 되며 메모리에서 해제된다.

미소유 참조

미소유 참조 역시 RC를 증가시키지 않는다. 미소유 참조는 옵셔널로 선언하는 것이 아니라 참조하려는 인스턴스가 메모리에 꼭 존재함을 기반으로 한다.

import UIKit

class Owner {
var name: String
var pet: Pet?
init(name: String) {
self.name = name
print("name: \(name) Owner init")
}
deinit {
print("name: \(self.name) Owner deinit")
}
}
class Pet {
var name: String
unowned var owner: Owner
init(name: String, owner: Owner) {
self.name = name
self.owner = owner
print("name: \(name) owner: \(owner.name) Pet init")
}
deinit {
print("name: \(self.name) Pet deinit")
}
}
var pepper: Owner? = Owner(name: "pepper") // count 1 for pepper
if let ownerPepper: Owner = pepper {
ownerPepper.pet = Pet(name: "mint", owner: ownerPepper) // count 1 for pet
}
pepper = nil // pepper의 카운트 -1, unowned인 Pet의 Pepper가 0이 되며 Pet인 mint도 -1이 되며 해제

중요한건 Owner는 Pet을 옵셔널 형태로 가지고 있다. Pet은 있어도 되고 없어도 되는 클래스이기 때문이다. 하지만 Pet은 Owner가 있어야만 존재할 수 있기 때문에 옵셔널이 아니고 unowned 키워드로 지정하여 미소유 참조를 해준다.

그래서 Owner인 pepper를 nil로 해주면 Pet 역시 메모리에서 해제된다.

참고

--

--

peppermint100
PEPPERMINT100

기억하기 위해 또는 잊어버리기 위해 작성하는 블로그입니다.