일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 360도 뷰어
- React-Native
- launchscreen
- 명시적 정체성
- data driven construct
- 앱 성능 개선
- privacyinfo.plist
- React Native
- 3b52.1
- 구조적 정체성
- panorama view
- 리액트 네이티브
- requirenativecomponent
- 네이티브
- 라이브러리 없이
- 360도 이미지 뷰어
- ios
- react-native-fast-image
- ssot
- 뷰 정체성
- 파노라마 뷰
- SwiftUI
- 스켈레톤 통합
- Android
- launch screen
- react
- 리액트
- native
- 360도 이미지
- 뷰 생명주기
- Today
- Total
Neoself의 기술 블로그
ARC와 약한 참조를 바탕으로 하는 메모리 관리 최적화 본문
ARC(Automatic Reference Counting)은 Swift에서 메모리 관리를 자동화하는 시스템입니다.
이름에서 짐작할 수 있듯, ARC는 각 객체가 참조되는 횟수를 추적함으로써, 객체가 필요하지 않을때 자동으로 메모리에서 객체를 해제하는 시스템인데, 이때 참조 횟수의 기준은 강한 참조를 의미합니다.
아래 코드에서는 몇번의 강한 참조가 발생하고 있을까요??
class HomeViewModel: ObservableObject {
private let todoService: TodoService
init(
todoService: TodoService = TodoService()
) {
self.todoService = todoService
}
// ... 다른 메서드들
}
struct HomeView: View {
@ObservedObject var viewModel: HomeViewModel
// ... 뷰 코드
}
먼저 class 키워드를 통해 참조타입임을 선언하였기 때문에, HomeViewModel은 ARC의 관리 범위에 포함됩니다. 데이터 통신을 다루는 class인 TodoService 인스턴스를 초기화 당시에 생성하였기 때문에, HomeViewModel이 존재하는 한, todoService 인스턴스도 메모리에 유지가 됩니다.
HomeView의 경우, @ObservedObject 프로퍼티 래퍼를 통해 HomeViewModel를 관찰, 즉 강하게 참조하고 있기 때문에, 뷰가 존재하는 한, 뷰모델 또한 메모리에 남아있게 됩니다.
즉, View는 HomeViewModel class를, HomeViewModel은 TodoService class에 대해 강한 참조를 하고 있습니다.
따라서 View가 처음 렌더링되면, HomeViewModel 그리고 todoService 인스턴스가 생성되며, 앱 종료 혹은 탭 변경으로 인해 View가 deinit되기 전까지는 3개의 객체가 메모리에 계속 남아있게 됩니다.
그럼 이제 강한참조를 바탕으로 하는 ARC 시스템의 동작 원리를 설명하겠습니다.
class Todo {
let title:String
init(title:String){
self.title = title
print("\(title)라는 내용의 Todo가 초기화되었습니다.")
}
deinit {
print("\(title)라는 내용의 Todo의 초기화가 해제되었습니다.")
}
}
위는 Todo라는 간단한 Class 정의문입니다. String 타입의 title을 지니고, 초기화, 그리고 초기화 해제 시 콘솔로 이를 전달합니다.
var data1: Todo?
var data2: Todo?
var data3: Todo?
data1 = Todo(title:"내일 오후 2시에 회의참가하기")
// "내일 오후 2시에 회의참가하기라는 내용의 Todo가 초기화되었습니다." 출력
3개의 Todo 옵셔널 타입을 지닌 변수 중 첫번째 변수로 클래스의 인스턴스를 생성하게 되면, 예상할 수 있듯 Todo 클래스는 초기화를 진행하며 참조 카운트가 1로 늘어납니다.
data2 = data1
data3 = data1
여기서 인스턴스를 data2와 data3에도 할당시키면, 참조 카운트는 1에서 3으로 증가합니다.
data1 = nil
data2 = nil
여기서 처음 Todo를 생성한 data1과 data2 변수에 nil을 할당하면, 참조 카운트는 1로 줄지만, Todo 객체는 여전히 data3에 남아있기 때문에 메모리에 존재합니다.
data3 = nil
// "내일 오후 2시에 회의참가하기라는 내용의 Todo 초기화가 해제되었습니다" 출력
이와 같은 맥락으로, data1, data2, data3에 모두 nil값을 할당하기 전까지, 즉 마지막 참조가 nil이 될때까지 Todo 객체는 메모리에 계속 남아있게 되며, 참조 카운트가 0이 되어서야 ARC는 객체를 메모리에서 해제합니다.
이러한 시스템으로 인해 ARC는 메모리 누수를 방지시켜줄 뿐만 아니라, 이미 해제된 메모리에 접근하는 상황을 막는 이점을 갖습니다.
하지만 2개 클래스의 인스턴스가 서로를 참조하는 상황이 발생하면 어떻게 될까요..?
class Todo {
let title: String
var assignee: User?
init(title: String) {
self.title = title
print("\(title) 내용의 Todo가 초기화되었습니다.")
}
deinit {
print("\(title) 내용의 Todo 초기화가 해제되었습니다.")
}
}
class User {
let name: String
var todos: [Todo]
init(name: String) {
self.name = name
self.todos = []
print("\(name) 사용자가 초기화되었습니다.")
}
deinit {
print("\(name) 사용자 초기화가 해제되었습니다")
}
}
// 인스턴스 생성
var todo: Todo? = Todo(title: "Complete project")
var user: User? = User(name: "John")
// 서로 간 참조 설정
todo?.assignee = user
user?.todos.append(todo!)
위 예시의 경우 Todo와 User 클래스는 서로에 대해 강하게 참조하고 있습니다. Todo는 assignee로 User를, 그리고 User는 todos 배열에 Todo를 갖고 있습니다.
todo = nil
user = nil
// 초기화 해제되지 않음 -> 메모리 누수 발생!
서로에 대한 강한 참조, 즉 순환 참조가 발생하고 있기 때문에, 2 변수에 nil를 할당해 참조를 해제하려 하여도, deinit가 호출되지 않고, 메모리 누수가 발생하게 됩니다...!
이러한 순환참조 현상을 방지하고자 사용하는 것이 약한참조입니다.
class Todo
let title: String
weak var assignee: User? // assignee를 약하게 참조
init(id: String, title: String)
self.title = title
print("Todo '\(title)' is being initialized")
}
deinit {
print("Todo '\(title)' is being deinitialized")
}
}
// User 클래스 유지
// 인스턴스 생성 및 참조 설정
var todo: Todo? = Todo(id: "1", title: "Complete project")
var user: User? = User(id: "1", name: "John")
todo?.assignee = user
user?.todos.append(todo!)
// 참조 해제
todo = nil
user = nil
// Todo에서 assignee를 약하게 참조하기에, user가 nil이 되면, 매가리없이 놓아준다...
// Todo 'Complete project' is being initialized
// User 'John' is being initialized
// Todo 'Complete project' is being deinitialized
// User 'John' is being deinitialized
user 변수에 nil을 할당하는 동시에, User 인스턴스의 참조 카운트가 0이 되며 메모리에서 해제가 됩니다. User 인스턴스가 해제되었기 때문에, Todo 인스턴스의 참조카운트도 자연스레 0이 되며 두 객체는 모두 메모리에서 해제, 즉 순환 참조 문제를 해결할 수 있게 됩니다.
이러한 약한 참조를 자주 사용하는 곳이 있는데요. 바로 Combine을 활용해 비동기 함수를 호출할 때입니다.
class HomeViewModel: ObservableObject {
private let todoService: TodoService
init(
todoService: TodoService = TodoService()
) {
self.todoService = todoService
fetchTodos()
}
func fetchTodos() {
todoService.fetchAllTodos(mode: sortOption)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
self.isLoading = false
} receiveValue: { [weak self] todos in // 여기!
guard let self = self else { return }
...
}
.store(in: &cancellables)
}
...
}
위 fetchTodos 함수는 홈화면에서 Todo들을 서버로부터 fetch해오는 비동기 함수입니다. todoService class에 있는 fetchAllTodos 함수를 호출한 후, Subscriber 역할을 하는 sink 메서드를 뒤에 연결하여, Publisher 역할인 fetchAllTodos가 방출하는 값을 수신 및 처리하는 클로저 함수를 실행합니다.
이때 Subscriber 클로저 내부에서 self 즉 HomeViewModel에 대해 약하게 참조하기 위해 [weak self]를 선언한 것을 볼 수 있는데요. [weak self]를 선언하지 않을 경우, 클로저는 HomeViewModel에 대해 강한 참조를 유지하기 때문에, 사용자가 홈화면에서 Todo 목록을 불러오는 도중에 탭을 변경하게 되어도, HomeViewModel 인스턴스가 메모리에서 즉시 해제되지 않습니다.
하지만, 약한 참조를 사용할 경우, 앞서 언급한 시나리오와 같이 HomeViewModel이 불필요하게 되면, 비동기 작업이 진행중이더라도 ARC를 통해 HomeViewModel을 즉시 메모리에서 해제할 수 있기 때문에 메모리 관리를 최적화할 수 있게 됩니다.
감사합니다.
'개발지식 정리 > Swift' 카테고리의 다른 글
SwiftUI에서 애플, 구글 로그인 구현하기 (with Node.js) (1) | 2024.10.09 |
---|---|
React Native와 비교해보는 Swift,SwiftUI (5) | 2024.10.08 |
Keychain과 UserDefaults를 활용한 사용자 정보 관리 (3) | 2024.10.07 |
Swift에서 싱글톤 패턴 활용하기 (0) | 2024.10.02 |
SwiftUI에서 MVVM + 서비스 레이어 패턴 구현하기 (1) | 2024.10.01 |