[iOS] Memory Debug Graph 분석하기
IT 동아리에서 2개월간 작은 서비스를 개발했다
기간이 짧았던 만큼 앱 규모도 작았지만, 그에 비해 메모리를 과다하게 사용하고 있다고 느껴졌다.
SwiftUI + Combine을 기반으로 프로젝트를 작업
우선 처음으로 의심해 본 부분은 메모리 릭이 발생하는 것 같다고 생각해서, 메모리 릭을 체크해봤다.
우선 프로파일링을 열어서 Leak을 체크
Leaks에 체크되는 부분은 없었지만 무언가 계속 증가하고 있었다.
그래서 그 다음에 생각되었던 부분은 앱 구조에서 무거운 객체를 여러번 생성하는게 아닐까 싶었다.
실질 개발기간이 약 1개월이었기에 (첫 배포까지는 약 3주) 각 Repository 및 UseCase에서 각 객체를 독립적으로 생성했다.
개발 단계에서 네이밍 변경 등 init(...)을 통해 주입하는 형태로 사용할 때 변화가 잦았어서 각 객체를 생성해서 빠르게 개발하는데 집중했었다.
초기 개발 단계에서 하나의 Repository 및 UseCase를 구현할 때 하나의 예시이다.
// MARK: - 초기 개발 단계
final class RepositoryImpl: Repository {
private let apiClient = APIClinet()
private let cache = UserStorage()
init() { }
func fetch() { ... }
}
struct UseCase {
let repository: Repository = RepositoryImpl()
init() { }
func execute() { ... }
}
이렇게 구현하다보니 UseCase 및 Repository가 존재할 때 마다 지속적으로 Repository 객체가 반복 생성되었고, cache에서 초기에 값을 load() 후 각 Repository에 저장하여 사용하는 형태였는데, 데이터의 싱크(무결성) 또한 깨지는 문제가 발생했다.
다른 무엇보다도 데이터의 무결성이 깨지는 문제는 로직 작성에 있어서 좀 치명적이었고, 배포 이후에는 거의 매일 기능 개발 및 리팩토링 함께 진행되고 있기에 점진적으로 수정하는 방향으로 진행했다.
// MARK: - 1차 수정
final class RepositoryImpl: Repository {
private let apiClient = APIClinet.shared
private let cache = UserStorage.shared
init() { }
func fetch() { ... }
}
우선 싱글톤으로 처리하게 된 이유는 프로젝트 규모가 작아 매일마다 새로운 기능이 붙을 때 영향 범위가 상대적으로 넓었고, 같이 작업하는 분의 기능 작업 브랜치와 충돌이 매우 클 수도 있다는 생각에 1차적으로 싱글톤 전환하기로 결정했다.
아래 이미지는 일차적으로 싱글톤으로 개선한 작업 그래프이다.
위의 사진은 1차적으로 개선사항으로 싱글톤을 적용한 부분에서 좌측을 확인해보자.
서로 다른 화면에 동일한 UseCase를 사용하고 있었다.
각 화면의 ViewModel에서 각 UseCase를 객체단위로 생성하고 있었기에 2차적으로 이 부분을 해결하고자 했다.
위의 그림에서보면 동일한 UseCase 객체가 2개인 것을 확인할 수 있다. 이에 따라 RepositoryImpl도 2개
이 상황을 해결하는데 고민을 정말 많이했었다.
- 1. 데이터의 무결성이 지켜져야 함.
- 2. 현재 프로젝트 상황 및 규모에 맞게 만들기.
결국 DIContainer를 만들어서 객체를 주입하는 형태로 가야했는데, 데이터의 무결성 부분이 많이 고민되었다.
우선 우리 앱은 SwiftUI를 베이스로 하고 있었기에 @StateObject를 root에서 주입하면 사실상 객체의 무결성도 지켜질 것으로 생각했다.
그래서 우선 아래와 같이 문제를 해결하기 위해 접근해 보았다.
아래는 그 예시이다.
// MARK: - 2차 개선사항
final class DIContainer: ObservableObject {
let profileRepository: ProfileRepository
let mbtiRepository: MBTIRepository
let matchingRepository: MatchingRepository
let subwayStationRepository: SubwayStationRepository
init() {
let apiClient = APIClient()
self.profileRepository = ProfileRepositoryImpl(apiClient: apiClient)
self.mbtiRepository = MBTIRepositoryImpl()
self.matchingRepository = MatchingRepositoryImpl(apiClient: apiClient)
self.subwayStationRepository = SubwayStationRepositoryImpl(apiClient: apiClient)
}
}
@main
struct FunchApp: App {
@StateObject private var appCoordinator = AppCoordinator()
var body: some View {
AnyView(...)
.environmentObject(diContainer)
}
}
struct ProfileViewBuilder {
private var diContainer: DIContainer
init(diContainer: DIContainer) {
self.diContainer = diContainer
}
var body: some View {
let useCase = DefaultDeleteProfileUseCase(profileRepository: diContainer.profileRepository)
let viewModel = ProfileViewModel(
useCase: useCase,
inject: diContainer.inject
)
let view = ProfileView(viewModel: viewModel)
return view
}
}
아래의 그림은 2차 개선 사항을 반영한 부분이다.
이제 ProfileRepositoryImpl을 확인해보자.
우선 그래프의 선 색상이
- 불투명한 흰색인 경우에는 강한 참조
- 약간 투명한 회식인 경우에는 약한 참조
아래처럼 개선했을 때 DIcontainer만 강한참조로 ProfileRepositoryImpl을 강한 참조로 들고 있고, ViewModel에서 사용하는 UseCase의 Repositorty 영역은 DIContainer에서 한번 생성된 객체를 사용하게 된다.
여기까지가 릴리즈 1.5.0에 포함되어 있다.
아직도 개선할 수 있는 부분이 많이 보인다. 네비게이션이나 여러 레이어에서 사용할 수 있는 것들을 Inject하는 형태로 수정해보고자 한다.
그리고 클린아키텍처는 여전히 어렵다. Persistence Data나 Network 관련한 부분은 구현까지 한곳에서 몰아서 관리하는게 더 편해 보이는데, 약간은 변형해서 사용하고 있고, 특히 아키텍처 그림에서 Service의 개념이 아직은 확실히 숙달되지 않은 느낌이다.
이번에 내용이 나중에 본다면 틀린 방법일 수 있지만, 다른 사람의 작업의 충돌 사항을 최소한으로 줄이면서 아키텍처를 개선하는 방향으로 딥하게 고민했던 시간이라 기록.
끝 -
'iOS프로젝트 > Funch(넥스터즈)' 카테고리의 다른 글
모듈화 리팩토링 과정에서 고민했던 것들 (2) | 2024.09.24 |
---|---|
SwiftUI 화면 dismiss 상황에서 흰 화면 나타나는 문제 (1) | 2024.09.22 |
Swift Concurrency를 적용하면서 발생한 동시성 문제 (0) | 2024.09.20 |
지하철 검색 기능에 캐싱 로직 도입하기 (0) | 2024.09.20 |
[IT 동아리 Nexters] 24기 프로젝트 회고 (0) | 2024.03.03 |