iOS프로젝트/Funch(넥스터즈)

[iOS] Memory Debug Graph 분석해 프로젝트 구조 개선

lgvv 2024. 3. 1. 01:33

[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차 개선


위의 사진은 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에서 한번 생성된 객체를 사용하게 된다.

 

2차 개선 후

 

 

여기까지가 릴리즈 1.5.0에 포함되어 있다.

 

아직도 개선할 수 있는 부분이 많이 보인다. 네비게이션이나 여러 레이어에서 사용할 수 있는 것들을 Inject하는 형태로 수정해보고자 한다.

그리고 클린아키텍처는 여전히 어렵다. Persistence Data나 Network 관련한 부분은 구현까지 한곳에서 몰아서 관리하는게 더 편해 보이는데, 약간은 변형해서 사용하고 있고, 특히 아키텍처 그림에서 Service의 개념이 아직은 확실히 숙달되지 않은 느낌이다.

 

이번에 내용이 나중에 본다면 틀린 방법일 수 있지만, 다른 사람의 작업의 충돌 사항을 최소한으로 줄이면서 아키텍처를 개선하는 방향으로 딥하게 고민했던 시간이라 기록.

 

끝 -