Skip to content

[기술 논의] DIContainer 도입

00me edited this page Nov 28, 2024 · 1 revision

문제 상황

현재 상황에서 DIContainer를 도입하는 것이 적절한지에 대해 논의가 필요했다.

의존성 주입을 누가 해줄 것인지, 모듈화되어 있는 구조에서 어떻게 의존성을 주입할 것인지 결정이 필요함


문제 해결

DIContainer 도입 계기

클린 아키텍처가 적용을 함에 따라 아래와 같이 뷰컨트롤러 하나를 띄우더라도 viewModel부터 useCase, Repository를 다 만들어주고 최종적으로 viewModel을 뷰컨트롤러에 넣어주면서 띄워준다.

let issueRepository = IssuesRepository()
let fetchIssuesUseCase = FetchIssuesUseCase(issueRepository: issueRepository)
let issueListViewModel = IssueListViewModel(fetchIssuesUseCase: fetchIssuesUseCase)
let issueListViewController = IssueListViewController(viewModel: issueListViewModel)
window?.rootViewController = UINavigationController(rootViewController: issueListViewController)

그러나, 모듈화가 되어있는 우리 프로젝트의 경우 Presentation 레이어에서 let issueRepository = IssuesRepository() 와 같은 코드는 작성할 수 없다.

위 코드와 같이 작성하려면 import MHData 를 해주어야 하는데, 그러면 Presentation 모듈이 Data 모듈을 의존하게 되므로 클린 아키텍처가 깨지게 된다.

따라서 DIContainer를 도입하여 SceneDelegate에서 미리 의존성들을 주입해두고 Presentation 계층에서 꺼내 쓰기로 결정하였다.


도입 과정

Swinject를 기반으로한 DIContainer를 모방하기로 했다.

싱글톤 DIContainer가 [프로토콜.Type: Any]를 통해 프로토콜과 구현체를 키밸류로 갖고 있는다.

구현하는 과정에서 두 가지 의문사항이 생겼다.

1. DIContainer에 ViewModel을 저장할 필요가 있나 ?

  • 뷰컨은 하나만 띄워서 보여질텐데, 뷰모델이 계속 DIContainer 안에 있는 것 자체로 힙 영역 메모리에 상주하고 있으므로 낭비가 될 것이라 생각함
  • DIContainer에 담겨있는 구현체의 경우 싱글톤처럼 사용되고 계속 재사용성이 되는데, 우리의 ViewModel은 정보를 유지할 필요도, 해당 화면을 조작하다가 닫을 경우 이전 정보를 항상 유지할 필요도 없으므로 계속 DIContainer가 갖고 있을 필요가 없음

2. ViewModel을 만들 때 생성자 파라미터로 UseCase를 받아야 하나 ?

  • 우리 프로젝트는 Coordinator가 없으므로 화면 전환이 일어나는 의존성 주입이 보통 아래와 같은 코드에서 발생한다.

    makingBookFloatingButton.addAction(UIAction { [weak self] _ in
    		let useCase = DIContainer.shared.reslove(HomeUseCase.self)
    		let viewModel = HomeViewModel(houseName: houseName, useCase: useCase)
    		let homeViewController = HomeViewController(viewModel: viewModel)
    		self?.navigationController?.pushViewController(homeViewController, animated: false)        
    }, for: .touchUpInside)

    이때 useCase를 뷰컨트롤러에서 DIContainer로부터 빼오는데, 이렇게 되면 viewController가 viewModel만 알면 되는데 useCase까지 알게되는 셈이다.

  • viewController에 useCase 의존성까지 둘 필요가 없으므로 ViewModel의 생성자가 동작하는 시점에 DIContainer에서 직접 꺼내오기로 결정했다.

3. DIContainer Resolve 실패 시 FatalError vs Throw vs Optional

1. 옵셔널 반환 (T?)

장점

  • 앱이 크래시 나지 않으며, 호출 측에서 안전하게 처리할 수 있음

단점

  • 호출하는 곳마다 nil 체크를 해야 하며, 이를 놓칠 경우 런타임 에러나 예기치 않은 동작이 발생할 수 있음
  • 의존성 주입 실패 시 문제를 조기에 발견하기 어려움

2. 에러 던지기 (throws)

장점

  • 호출 측에서 명시적으로 에러 처리를 강제하므로, 에러 상황을 놓치지 않고 처리할 수 있음
  • 에러 타입을 통해 상세한 오류 정보를 전달할 수 있음

단점

  • 에러 처리를 위한 코드가 추가되어 복잡도가 증가할 수 있음
  • 비동기 코드나 클로저 내부에서는 에러 처리가 다소 번거로울 수 있음

3. fatalError 사용

장점

  • 의존성 누락 등 치명적인 오류를 즉시 발견할 수 있어 개발 단계에서 문제를 빠르게 해결할 수 있음
  • 코드가 간결함

단점

  • 앱이 크래시 나므로 사용자 경험에 부정적 영향을 미침
  • 프로덕션 환경에서 앱이 예기치 않게 종료될 수 있음

3번에 대한 정리 및 결론

에러를 던지는 것으로 결정

이유

  • 에러 처리를 강제함으로써, 의존성 주입 실패 시 발생할 수 있는 문제를 사전에 방지할 수 있다고 판단
  • 에러 타입을 커스텀하여 상세한 정보를 제공하면 디버깅과 로깅에 도움됨
  • 앱이 크래시 나지 않으면서도 오류 상황을 명확하게 처리할 수 있음

아래와 같이 사용할 수 있음 !

public final class HomeViewModel {
    let houseName: String
    private let useCase: UseCaseProtocol

    public init(
        houseName: String,
        useCase: UseCaseProtocol = try DIContainer.shared.resolve(UseCaseProtocol.self)
    ) throws {
        self.houseName = houseName
        self.useCase = useCase
    }
}

결론

  1. DIContainer 도입 이유

클린 아키텍처를 유지하기 위해, Presentation 계층에서 Data 계층의 의존성을 직접 생성하지 않고 DIContainer를 통해 해결함

  1. ViewModel 관련 결정

ViewModel은 DIContainer에 저장하지 않고, 생성자에서 필요한 UseCase를 DIContainer로부터 직접 가져오도록 처리

  1. DIContainer의 Resolve 실패 처리

throws를 채택하여 의존성 주입 실패 시 명시적인 에러 처리를 강제하고, 디버깅과 유지보수를 용이하게 함

  1. 결론

앱 안정성을 높이고 의존성 관리와 코드의 분리도를 개선하며, 클린 아키텍처의 원칙을 유지


참조 링크

https://github.com/boostcampwm-2024/swift-p3-issue-tracker/pull/184#discussion_r1782120637

Clone this wiki locally