DIContainer 맛보기

·2025년 10월 20일

iOS-posting

목록 보기
7/10
post-thumbnail

아카데미 최종 프로젝트 중에 살짝 거슬리는 문제를 마주했습니다.

  • 검색관련 기능을 개발중이었는데, SearchViewModel과 그 최근검색을 가져오는 로직을 갖고 있는 RecentSearchService간의 의존성 주입 문제였습니다.

앱에서 일반적인 SearchViewModel은 다음과 같았습니다.
불필요한 부분 날려주긴했는데, RecentSearchService를 중점으로 보시면 됩니다!


```swift
@Observable
class SearchViewModel {

    private var recentSearchService: RecentSearchService?
    var recentSearchLists: [SearchableItem] = []

    func setRecentSearchService(_ service: RecentSearchService) {
        recentSearchService = service
        fetchRecentSearch()
    }

    func fetchRecentSearch() {
        recentSearchLists = recentSearchService?.fetch() ?? []
    }
}

왜 recentSearchService가 옵셔널이냐?

그게 참 문제입니다..
이게 RecentSearchService는 여러가지 소망을 담은 친구였는데요..
하고 있는건 많지않습니다.
1. ModelContext를 가져온다.
2. Swiftdata로 RecentSearch , 즉 최근 검색어 리스트를 가져온다.
3. 경로 탐색(아직 미구현)을 시작하면은 최근 검색 리스트에 등록한다.

이렇게 딱 3가지가 있는데, 아시다시피 environment로 의존성을 주입한 modelContext는 View딴에서만 불러올 수가 있는지라..
쉽게 설명하면 아래와 같습니다..!

그래서 생긴 문제가, 이게 SwiftData에 접근하는 modelContext가 SearchViewModel에 들어가고, 거기에서 한번 더 넘겨줘야하는 상황이라 억지로 넘겨주느라 RecentSearchService를 옵셔널로 한 다음,

// SearchViewModel.swift
 func setRecentSearchService(_ service: RecentSearchService) {
        recentSearchService = service
}

여기서 생성 시점에 만들어주고 있는데, SearchView에서는 아래처럼 onAppear시점에 searchViewModelsetRecentSearchService로 넣어줬습니다..!

  .onAppear {
            let recentService = RecentSearchService(modelContext: modelContext)
            searchViewModel.setRecentSearchService(recentService)
        }

나름대로 머리를 짜낸 방법인데, 아무래도 이건 이상적인 방법이 아니겠지요..


사실 지금 대로는 잘 굴러가긴하는데, 이제 SearchView뿐만아니라 MainView에서 최근검색결과를 가져와야하는 상황에 이르자, 새로운 방식의 필요성을 느꼈습니다..!

해결과제

  1. onAppear로 말도안되게 넘겨주는거를 개선하자!
  2. SearchView에만 특화된 지금의 의존성 주입 로직을 버리고, MainView에서도 문제없이 SearchService를 활용할 수 있게하자!

사실 이걸 해결하려면 그냥 onAppear 대신에 좀 더 부드러운 방식이 있겠거니? 정도를 생각했던 것 같은데, 좀 더 검색해보니 좀 더 거대한 솔루션들을 많이 발견할 수 있었다.

그중에 하나가 DIContainer를 통한 의존성 주입이었다.

DIContainer란?

필요한 객체들을 미리 등록해두고, 나중에 쉽게 꺼내 쓰게 도와주는 Container

일단 백문이 불여일견이라고 실제 코드와 함께보면 이해가 쉬울 것이다..!

일단 기본적인 보일러플레이트는 다음과 같다.

생성

final class DIContainer {

	// 언제든지 불러와서 쓸 수 있게 싱글톤으로 생성
    static let shared = DIContainer()

    private init() {}
	//키와 만들 객체들을 매핑해줄겁니다..! 키를 입력하면 그에따른 객체를 뱉을 수 있게끔
    private var dependencies: [String: Any] = [:]
	
    func register<T>(type: T.Type, component: Any) {
        let key = String(describing: type)
        dependencies[key] = component
    }

    func resolve<T>(type: T.Type) -> T {
        let key = String(describing: type)
        guard let component = dependencies[key] as? T else {
            fatalError("\(key)가 등록되지 않았습니다. 확인하세요.")
        }
        return component
    }
}

그래서 이걸 나와있듯이 register 를 통해서 저장 해주고, resolve를 통해서 빼다쓰면 될 것입니다..!
key 매핑시 문제가 없도록하기위해서 String(describing:)을 써줬는데, 이게 타입형태로 받아주고 있으니까 SearchService.self를 입력하면 -> SearchService가 되는 것이다. 이러면 enum같은거 안쓰고도 자동완성을 통해서 코딩이 가능하니 안정적인 개발이 가능할거예요.

활용 (등록과정-register)

자 그러면이제 어떻게 활용을 해줄 것이냐인데,
DiContainerregisterDependencies() 를 호출하면 그때부터 의존성이 한번에 등록이 되는겁니다! 그 시점부터 이제 컨테이너가 생성된거라 앱 어디서든 접근할 수 있는 상태가 됩니다.
이안에서 Schema 뿐만아니라 ModelContainer까지 함께 생성하고 필요한 서비스를 모두 컨테이너에 등록을 해줬습니다.

이후에는 굳이 ViewModel을 거치지 않더라도, 필요한 객체를 직접 주입받아 사용할 수 있습니다 !

extension DIContainer {
    static func registerDependencies() {
        let container = DIContainer.shared

        let schema = Schema([RecentSearch.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
        let modelContainer = try! ModelContainer(for: schema, configurations: [config])

        container.register(type: ModelContainer.self, component: modelContainer)
        container.register(type: RecentSearchService.self, component: RecentSearchService(modelContext: modelContainer.mainContext))
        container.register(type: SearchProviding.self, component: SearchService())
        container.register(type: MainViewModel.self, component: MainViewModel())
    }

    static func registerForPreview() {
        ...  생략 ,  isStoredInMemoryOnly: true 정도 차이 
    }
}

활용 (사용과정-resolve)

private let recentSearchService = DIContainer.shared.resolve(type: RecentSearchService.self)

골머리를 앓았던 ViewModel에서 Service에 modelContext넣어주기가 이렇게 쉽게 가능해진 것을 볼 수가 있었습니다..!
DIContainer에서 등록부분을 따라가보면

container.register(type: RecentSearchService.self, component: RecentSearchService(modelContext: modelContainer.mainContext))

이렇게 되어있는데, 써있는그대로 modelContainer.mainContext로 DIContainer를 통해서 바로 등록을 해준 상황이라 쉽게 적용이 될 수 있었던 것입니다..!


후기

  • 거창하게 DIContainer를 통해 해결했어요! 하는 글이었지만, boilerplate덕분에 최대한 기존 문제를 해결할 수는 있었는데, 아직 상당부분 개념이 미흡하다는 것을 느꼈다.

  • 무언가를 공부하거나 문제를 해결할 때, 단순히 <어떻게 잘 다루는가>보다 먼저 선행되어야 하는 건 <이런 개념이 있구나> 하고 인식할 수 있는 힘인 것 같다.

  • DIContainer라는 존재를 잘 모를때는 여기에 쓰이는 것인지도 몰랐고, 좀 더 고민했던 부분에서 시작해서 찾아보니, 더 쉽고 최적화된 방법을 찾을 수 있었다.

  • 사실 AI를 사용한다면 당장의 문제해결은 어렵지않을 것이다. 다만 지속가능한 개발이 되기 위해서는 <이럴땐 어떻게 해결할 수 있는지> 문제를 직면할대마다 <어떤것을 공부하면 되겠다!> 하는 감각을 익히는게 중요하다고 느꼈다. 때문에 많은 삽질을 해봐야하는구나 그래야 이후에 더 큰 상황에 삽질을 덜할 수 있겠다 정도를 느꼈던 것 같다.

결론: 다 찍먹해봐야한다..!

다음엔 아마 이번에 서칭하면서 알게된 @Inject가 되지않을까?

profile
기억보단 기록을

0개의 댓글