일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- 네트워크
- uikit
- RxSwift
- 이론
- mvc
- CS
- 반응형
- 대표
- 옵셔널
- Optional
- 학과별커뮤니티
- 구름톤 유니브
- 토이프로젝트
- 실습
- collectionview
- struct
- async
- MVVM
- 스트럭트
- WeatherKit
- Kingfisher
- SwiftUI
- 세종대학교
- 앱개발
- 동시성
- swift
- 프로토콜
- ios
- 기초문법
- GCD
- Today
- Total
스윞한 개발자
iOS - ARC(Swift 메모리 관리 기법) 본문
안녕하세요! 이번 포스팅에서는 ARC에 대해 정리해 보고 무작정 weak를 쓰던 그 시절... 에서 벗어나 언제 어떤 상황에 써야 하는지 그 예시도 함께 살펴보겠습니다.

오늘 정리할 내용은 ARC! Automatic Reference Counting, 자동 참조 계수입니다.
저도 프로젝트를 진행하면서 강한 참조 약한 참조 많이 사용도 해보았는데요! 언제 어떻게 왜 쓰는지는 정확하게 몰랐습니다!
이번 시간을 통해 더 자세히 제대로 알아보겠습니다!
ARC에 대해 알아보기 전에 RC(Reference Count)에 대해서 한번 정리해 보겠습니다.
#RC
인스턴스를 참조하는가 안 하는가를 숫자로 표현한 것입니다. 컴파일 타임에 언제 참조되고 해제되는지 결정되며, 런타임 때 그대로 실행됩니다.
* 장점 : 개발자가 참조 해제 시점을 파악 가능, 런타임 때 추가 리소스 발생 X
* 단점 : 순환 참조가 발생할 경우, 영구적으로 메모리가 해제되지 않을 수 있음
간단하게 RC가 무엇인지 정리해 보았습니다. 이제 ARC를 들어가기 전에 MRC에 대해 혹시 들어보셨나요?
#MRC
MRC란, Obj-C를 사용할 때에는 retain, release, autorelease 등을 통해서 수동적으로 메모리를 관리했습니다.
(Manual Reference Counting)
retain(retain Count). 즉, reference Count 참조 증가를 통해 현재의 객체가 유지되는 것을 보장시키고! release(retain Count). 다시 이 참조 감소를 시켜 retain후 필요 없을 때는 release를 해주며 수동적으로 이 메모리에 대해 관리를 해줬습니다!
참 귀찮고,, 쉽지 않은 작업이었던 것 같습니다. 하지만! 또 이렇게 번거로움을 줄이고자 2011 WWDC에서 자동으로 메모리 참조에 대해 계산/관리해 주는!! ARC가 등장하게 되었습니다.
그래서 Swift에서는 ARC를 통해 메모리 관리는 하게 되었습니다.
# ARC
ARC(Automatic Reference Counting)
Reference Count. 참조 횟수를 계산하여 참조 횟수가 0일 경우, 더 이상 사용하지 않는 메모리라 생각하여 해제합니다.
메모리 영역 중 Heap은 클래스의 인스턴스, 클로저 등을 저장합니다. Heap영역에 저장되는 메모리는 직접 할당하고 해제할 수 있습니다.
ARC는 인스턴스를 참조하는 횟수만큼 RC를 증가시키고, 인스턴스가 해제될때 RC를 감소시킵니다. 더 이상 참조하고 있는 것이 없다면 ARC는 해당 인스턴스가 필요 없다고 판단하여 자동으로 메모리에서 해제됩니다.
이 계산은 Compile Time에 자동으로 retain, release를 적절한 위치에 삽입하여 효율적으로 관리할 수 있게 된 것입니다!!
* 동적 할당으로 object가 생성되면 해당 정보는 HeapObject라는 구조체로 관리
* HeapObject 안에는 참조 계수 포함
* class에 대한 HeapObject를 통해 참조 계수 관리 가능
기본적으로 클래스의 객체를 가리키는 각각의 참조는 강한 참조입니다.
(최소한 하나 이상의 강한 참조가 있는 한 이 객체는 메모리는 해제되지 않습니다. 즉, 객체에 대한 강한 참조가 존재하지 않는다면 메모리에서 해제됩니다.)
개발자들이 불편하게 해 왔던 MRC에서 ARC가 나오게 되면서 정말 많이 편해졌는데, 이렇게 좋은 ARC를 사용할 때도 주의해서 써야할 점이 있습니다!
대부분의 경우에서는 이 ARC가 메모리를 알아서 관리해 주지만, 아래의 상황의 경우에는 많이 들어왔던 메모리 누수(Memory Leak)가 발생하게 됩니다. 그렇기 때문에 적절한 위치에 해결법을 추가하여 누수가 발생하지 않도록 잘 사용해야 합니다.
# 순환 참조
두 객체가 서로가 서로를 참조하고 있는 형태를 순환 참조라고 합니다. 순환 참조가 발생 시 영구적으로 메모리가 해제되지 않을 수 있습니다. 하지만 강한 참조 Strong으로 선언된 변수들이 순환 참조가 됐을 경우 RC가 0이 되지 못하면서 "강한 순환 참조"가 발생할 수 있습니다. 이 경우 메모리 누수(Memory Leak)가 발생할 수 있습니다. 이러한 문제가 발생하는 것을 막기 위해 이 연결 상태를 strong, weak, unowned으로 상황에 맞게 결정할 수 있는데 이것이 바로 Retain Cycle입니다.
위의 표와 같이
1. Strong
인스턴스의 주소값이 변수에 할당될 때 RC가 증가하면 강한 참조입니다. default값이기도 합니다.
2. Weak
인스턴스를 참조할 경우 RC를 증가시키지 않습니다. 인스턴스가 메모리에서 해제된 경우 자동으로 nil이 할당되어 메모리가 해제됩니다.
weak(약한 참조)는 무조건 옵셔널 한 타입의 변수여야 합니다.
3. Unowned
미소유 참조 unowned은 weak와 같이 강한 순환참조를 해결할 수 있고, RC를 증가시키지 않습니다. 하지만 옵셔널의 강제 언래핑처럼 메모리에서 해제된 경우 nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있습니다. 그렇기에 미소유 참조로 선언된 인스턴스가 해제되었는데 접근하게 될 경우 런타임 에러가 발생할 수 있습니다.
그렇다면, 언제 어떤 상황에서 Retain Cycle을 적절히 사용해야할까요?
final class Guild {
var name: String
weak var owner: User?
init(name: String) {
self.name = name
print("Guild init")
}
deinit {
print("Guild Deinit")
}
}
final class User {
var nickname: String
weak var guild: Guild?
init(nickname: String) {
self.nickname = nickname
print("User Init")
}
deinit {
print("User Deinit")
}
}
위의 코드를 먼저 살펴보겠습니다. 길드와 유저라는 클래스가 선언되어 있습니다. 게임을 하려면 유저는 길드에 가입이 되어있는 경우도 있고, 길드에서는 유저에 대한 이름도 가지고 있습니다. 그렇기 때문에 각 class 내에서는 서로의 프로퍼티를 각각 가지고 있습니다.
var customGuild: Guild? = Guild(name: "1등길드") // Rc + 1
var character: User? = User(nickname: "스윞남") //RC + 1
//순환참조 발생 -> RC - 1 가 발생하지 않아! Deinit이 발생하지 않음
customGuild?.owner = character //RC + 1
character?.guild = customGuild //Rc + 1
//먼저 해제를 시켜 해결! -> 현실적으로 불가능한 방법 => weak, unowned -> RC 증가시키지 않는다.
character?.guild = nil
customGuild?.owner = nil
//인스턴스의 수명 종료
customGuild = nil //Rc - 1
character = nil //RC - 1
위처럼 각각의 인스턴스에 변수를 할당하게 되면서 기존의 초기화 선언 때 증가했던 RC가 한번 더 +1이 되게 됩니다.
만약 각 인스턴스에 nil로 먼저 해제를 시켜준다면 정상적으로 deinit이 될 것입니다. 지금 코드의 경우에는 그 횟수가 적어 가능하지만, 실제 코드 구현에서는 방대한 양의 코드에 적절한 위치 삽입해 주는 작업은 사실상 불가능할 것입니다.
그래서 최초 코드에 있던 weak var를 선언해 주게 됩니다. weak로 선언하게 된다면, 참조가 발생하더라도 RC를 증가시키지 않고 런타임 시점에서 nil로 반환될 가능성이 있기 때문에 변수인 var로 선언하게 됩니다.
다음 예시를 살펴보겠습니다. 개발할 때 많이 사용하는 tableView, collectionView, 프로토콜 기반 역값전달과 관련된 예시입니다.
protocol customProtocol: AnyObject {
func numberOfRowsInSection()
}
final class Main: customProtocol {
func numberOfRowsInSection() {
print(#function)
}
lazy var tableView = {
let view = TableView()
print("Detail 클로저 즉시 실행")
view.delegate = self
return view
}()
init() {
print("Main Init")
}
deinit {
print("Main Deinit")
}
}
/*
tableView, collection
프로토콜 기반 역값전달
person property name. 클로저 구문
*/
final class TableView {
var delegate: customProtocol? // RC 증가
func setup() {
print(#function)
delegate?.numberOfRowsInSection()
}
init() {
print("Detail Init")
}
deinit {
print("Detail Deinit")
}
}
우선 위의 코드를 차근차근 살펴보겠습니다. CustomProtocol이라는 프로토콜을 선언하였고, AnyObject를 사용해 class만 사용가능하게 제약을 걸어주었습니다. 참조와 관련된 데이터는 힙 영역에서 관리를 하는데, 힙 영역에서는 클래스/클로저만이 사용되기 때문에 구조체, 열거형 등의 값타입이 사용되지 못하게 막아주는 과정을 추가했습니다!
저희가 많이 사용하는 tableView를 예시로 보면, 테이블뷰 내부에는 delegate를 실행시키는 setup이라는 코드가 있다고 가정해 보겠습니다. 상위 뷰인 Main에서 tableView를 초기화시키고 클로저 구문을 통해 즉시 실행시킵니다.
var main: Main? = Main() //init
main?.tableView
main = nil //deinit
main?.tableView
결과를 출력하기 위해 위의 코드를 실행시켜 보면,
Main과 Detail에서 Deinit이 되었음을 알리는 로그가 출력되지 않았습니다. Main에서의 delegate를 참조하고 delegate이 Main의 self를 참조하게 되어 순환참조가 발생하게 됩니다. 그렇기 때문에 delegate에 weak를 선언해 약한 참조를 통해 RC를 증가시키지 않습니다.
Main과 TableView 모두 deinit이 되었음을 확인할 수 있습니다.
다음 예시를 살펴보겠습니다.
final class Person {
var name: String
//self.name: 인스턴스가 없어져도 name은 살아있는 상황
lazy var introduce = { [weak self] in //(매개 변수 없고 이름도 없고)클로저
print("introduce", self?.name)
}//()<- 함수 호출이 되어버림!
init(name: String) { //인스턴스 생성 시점
self.name = name
print("Person init")
}
deinit {
print("Person deinit")
}
func hello() {
print(#function)
}
}
name이 초기화되기 전의 introduce 변수는 name이라는 변수를 가지기 위해 lazy 하게 선언됩니다. 또한 클로저 내부에서는 자기 자신의 변수에 대해 self를 써주어야 합니다. 만약 클래스가 없어지더라도, 자신이 가지고 있는 값에 대해서는 출력을 하기 위해 self를 통해 값을 강하게 가지고 있겠음을 의미합니다.
이처럼 강하게 참조하고 있을 경우, class가 사라지더라도 프로퍼티가 메모리에서 사리 지지 않게 됩니다. 메모리에서 해제를 해야 하는 상황에서는 이미 사라졌는데 어디서 어떤 방식으로 접근할지 모르기 때문에 메모리에서 영영 사라지지 않게 되고, 이 경우에 메모리 누수(Memory Leak)가 발생하게 됩니다.
이 경우에도 [weak self] 약한 참조를 통해 해결이 가능합니다. 이렇게 되면 클래스가 없어질 때 그 안의 프로퍼티도 없어지겠다 -> nil로 반환되겠다고 선언하게 되며 위의 강한 참조의 문제를 해결할 수 있습니다.
왜 강한/순환참조가 발생하면, 메모리 누수가 발생하는가?
* 인스턴스가 nil인건 메모리에 있지 않습니다. 하지만, 강하게 참조할 경우 이 인스턴스가 살아 있기 때문에, 프로퍼티를 찾아서 메모리에 사라지게 해야 하는데 인스턴스가 사라졌기 때문에 찾아서 사라지게 할 수 없습니다. 그래서 그 공간은 쓸모없이 메모리를 차지하며 메모리 누수가 발생하게 됩니다.
* RC가 불필요하게 증가하게 되어 인스턴스가 메모리 해제 시점에서 증가한 RC에 대해 감소하지 않기 때문에 메모리 누수가 발생할 수 있습니다.
이번 시간에는 ARC에 대해 공부하며 정리해 보았습니다!
잘못된 점이나 부족한 점에 대한 피드백은 언제나 환영입니다. 다음 포스팅에는 더 양질의 포스팅으로 찾아오겠습니다!

아래의 글들을 참고해 작성되었습니다.
https://www.thomashanning.com/retain-cycles-weak-unowned-swift/
Retain Cycles, Weak and Unowned in Swift
Memory management, retain cycles and the usage of the keywords weak and unowned are a little bit confusing. On the other hand it’s very important to understand this topic properly because retain cycles are one of the major reasons for memory problems.
www.thomashanning.com
https://leeari95.tistory.com/21
[Swift/iOS] 메모리를 관리해주는 ARC에 대해 알아보자.
🔍 Swift는 메모리 관리를 어떻게 할까? ARC(Automatic Reference Counting)를 사용한다. 🔍 RC(Referenc Count)란 무엇인가? 인스턴스를 현재 누가 가르키고 있느냐 없느냐(참조하냐 안하냐)를 숫자로 표현한
leeari95.tistory.com
[Swift] 메모리 관리하기
오늘은 Swift로 메모리관리를 어떻게 하는지에 대해서 간단하게 적어볼게요!! 아래의 목차를 따라 적어보았습니다! Swift에서의 메모리 관리 Retain cycle Weak 키워드 Retain cylce in Delegate Retain cycle in Clo
wodyios.tistory.com
'Swift 이론' 카테고리의 다른 글
DAO, DTO, Entity + Repository Pattern (2) | 2025.02.17 |
---|---|
DiffableDatasource의 이해 (0) | 2025.02.16 |
Actor 톺아보기 👀 (0) | 2025.02.04 |
이미지 리사이징 vs 이미지 다운 샘플링 (0) | 2025.02.03 |
Swift Concurrency(with GCD) (0) | 2025.01.23 |