일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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
- custom ui
- swift 백준
- task cancellation
- button configuration
- swift navigationcontroller
- rxdatasources
- claen architecture
- SWIFT
- swift bottomsheet
- traits
- swift 점선
- swift dashed line
- Tuist
- DP
- swift concurrency
- identifiable
- custom navigation bar
- coordinator
- task cancel
- custombottomsheet
- swift custom ui
- paragraph style
- reactorkit
- 버튼 피드백
- BFS
- domain data
- RxSwift
- uikit toast
- Today
- Total
김경록의 앱 개발 여정
[Swift] coordinator 패턴 사용시 memory leak 주의점 본문
코디네이터 패턴은 iOS 애플리케이션에서 화면 전환 및 흐름 제어를 관리하는 데 매우 유용한 패턴입니다.
다양한 형태로 구현할 수 있지만, 여기서는 제가 사용하는 형태를 공유하고, 코디네이터 패턴을 구현하면서 발생했던 실수와 그에 따른 주의사항을 정리해 보았습니다.
코디네이터의 기본 형태
public protocol Coordinator: AnyObject {
var parentCoordinator: Coordinator? { get set }
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
func didFinish()
}
위와 같은 구조를 바탕으로 코디네이터 간의 부모-자식 관계를 정의하고, 화면 흐름을 명확히 제어할 수 있습니다.
코디네이터 패턴 사용 시 주의해야 할 점
1. 뷰 컨트롤러는 코디네이터를 약한 참조로 가져야 한다
아래는 잘못된 구현 예입니다:
final class TestViewController: UIViewController {
let coordinator = DefaultTestCoordinator()
}
이 코드에서 TestViewController는 DefaultTestCoordinator(구현체)를 강한 참조로 가지고 있습니다. 이는 두 가지 문제를 일으킬 수 있습니다:
메모리 누수: TestViewController와 DefaultTestCoordinator 간에 강한 참조 순환이 발생하여 메모리에서 해제되지 않습니다.
구조적 문제: 뷰 컨트롤러가 코디네이터 구현체에 의존하고 있어 의존성 역전 원칙(DIP)을 위배합니다.
아래는 개선된 코드입니다.
// 코디네이터 프로토콜
protocol TestCoordinator: Coordinator {}
// 코디네이터 구현체
final class DefaultTestCoordinator: TestCoordinator {
// 코디네이터 구현...
}
// 뷰 컨트롤러
final class TestViewController: UIViewController {
// 구현체가 아닌 추상체에 약한 참조로 의존
weak var coordinator: TestCoordinator?
}
추상화 의존: TestViewController는 DefaultTestCoordinator 구현체가 아닌 TestCoordinator 프로토콜에 의존합니다.
약한 참조: weak 키워드를 사용하여 강한 참조 순환을 방지합니다.
2. 뷰 컨트롤러 인스턴스 변수 선언 시 주의점
코디네이터의 start() 메서드에서 뷰 컨트롤러를 생성하는 방법 또한 주의가 필요합니다.
잘못된 예시
final class DefaultTestCoordinator: TestCoordinator {
let testViewController = TestViewController()
func start() {
testViewController.coordinator = self
navigationController.pushViewController(testViewController, animated: true)
}
}
문제점:
testViewController가 DefaultTestCoordinator의 전역 변수로 선언되어 있습니다.
여러번 사용되는 경우가 있었고, 코드의 중복을 줄이기 위해 무의식적으로 해당 방식을 선택한 적이 있습니다.
이 경우, 뷰 컨트롤러가 pop되더라도 여전히 DefaultTestCoordinator에 의해 강한 참조가 유지되므로 메모리에서 해제되지 않습니다.
올바른 개선 방법
1. 뷰 컨트롤러를 지역 변수로 생성
final class DefaultTestCoordinator: TestCoordinator {
func start() {
let testViewController = TestViewController()
testViewController.coordinator = self
navigationController.pushViewController(testViewController, animated: true)
}
}
testViewController는 start() 메서드 내에서만 존재하며, 뷰 컨트롤러가 pop되면 자동으로 메모리에서 해제됩니다.
- 옵셔널 타입으로 선언 후 초기화
final class DefaultTestCoordinator: TestCoordinator {
var testViewController: TestViewController?
func start() {
let testViewController = TestViewController()
testViewController.coordinator = self
navigationController.pushViewController(testViewController, animated: true)
self.testViewController = testViewController
}
func didFinish() {
testViewController = nil
}
}
장점: testViewController를 코디네이터의 속성으로 저장해도, didFinish()에서 명시적으로 nil로 설정하여 강한 참조를 끊을 수 있습니다.
마치며..
뷰 컨트롤러는 코디네이터를 weak 참조로 가져야 한다
이를 통해 강한 참조 순환을 방지하고, 의존성 역전 원칙을 준수할 수 있습니다.
뷰 컨트롤러 인스턴스 생성은 지역 변수 또는 관리 가능한 방식으로 처리해야 한다
전역 변수로 뷰 컨트롤러를 선언하면 메모리 누수가 발생할 수 있으므로 지역 변수로 선언하거나, 필요 시 옵셔널로 선언하여 수동으로 참조를 해제합니다.
메모리 누수를 예방하는 습관을 들이자
메모리 디버거를 사용하여 참조 관계를 주기적으로 점검하고, 코디네이터와 뷰 컨트롤러 간 강한 참조가 없는지 확인하는 것이 중요합니다.
사실 모두 기본적인 강한 순환 참조의 예시지만,
이러한 점을 준수하면 더욱 안전하고 효율적인 코디네이터 패턴을 구현할 수 있습니다! 😊
'TIL' 카테고리의 다른 글
[Swift] existential type과 any키워드(Boxed Protocol Type) (0) | 2025.01.09 |
---|---|
[Swift] 스위프트 패러다임 (0) | 2025.01.09 |
[iOS] Xcode Build System (0) | 2025.01.08 |
[CS] 다이나믹 라이브러리 vs 스태틱 라이브러리 (0) | 2025.01.08 |
[Swift] Tuist를 통한 ATS 처리 (0) | 2025.01.08 |