hyunn

[iOS] DI(의존성 주입)에 대한 고찰 (feat. Clean Architecture + MVVM) 본문

iOS

[iOS] DI(의존성 주입)에 대한 고찰 (feat. Clean Architecture + MVVM)

hyunn383 2024. 7. 17. 23:59

✨ DI에 대해 글을 쓰게된 계기

Clean Architecture를 공부하면서 항상 어려웠던 개념이 바로 Dependency Injection이었다.

그래서 오늘은 DI에 대해서 내가 생각하고 고민했던 과정들과 결과를 글로 정리해보려고 한다.


🛠️ Clean Architecture + MVVM

이전 포스팅에서도 언급했듯이, 클린아키텍쳐를 프로젝트에 적용해본 적이 있다.

정확히 말하자면 Clean Architecture에 MVVM패턴이 적용된 아키텍쳐이다.

 

우선 클린아키텍쳐 컨셉에 맞게 Presentation Layer, Domain Layer, Data Layer로 계층을 나누었다.

Presentation Layer는 UI를 담당하는 계층이다. MVVM패턴을 적용하여 ViewModel을 두었다.

Domain Layer는 앱의 비즈니스 로직을 담당한다. 비즈니스 로직이 작성된 UseCase와 앱에서 사용할 데이터 모델인 Entity를 두었다.

Data Layer는 서버와 통신하여 외부에서 데이터를 가져오는 계층이다. 서버와 통신하는 로직이 작성된 Repository와 서버에서 응답해주는 데이터 모델인 DTO를 두었다.


🚫 의존성 규칙을 어기다

이렇게 각각의 레이어들을 다 구현하고 나니, 의문이 들었다.

클린아키텍쳐의 컨셉에는 레이어를 분리하는 것 뿐만 아니라 "의존성 규칙" 이라는 것이 있다.

 

(유명한 깃허브 레포지토리이다. 해당 레포에 자세히 설명되있다. 참고하면 좋다.)

https://github.com/kudoleh/iOS-Clean-Architecture-MVVM

 

GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor

Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...

github.com

 

 

Clean Architecture의 의존성 규칙에 따르자면, Domain Layer는 다른 레이어를 의존하면 안된다. 

Presentation Layer -> Domain Layer <- Data Layer

 

의존성 방향은 이렇게 흘러야한다.

 

하지만, 의존성 규칙 따위는 신경안쓰고 각각의 레이어들을 구현하다보니, Domain Layer가 Data Layer를 참조하고 있었다!!

앱의 비즈니스 로직을 구현하려면 어쨋든 서버와의 통신으로 받아온 데이터들을 가지고 로직을 짜야하는데,
그러면 당연히 Domain Layer의 UseCase에서 Data Layer의 Repository의 함수를 호출할 수 밖에 없다.
DI가 뭔지 몰랐던 그 당시의 나는... UseCase에서 Repository객체를 생성해서 객체 내부의 함수를 호출했다.
Domain Layer가 Data Layer를 너무나도 의존하고 있었다.

 

 

이해하기 쉽게 코드로 예시를 들자면 아래와 같다.

class UserUseCase {
    func getMyProfile(accessToken: String) -> Observable<Execution<BaseResponse<MyProfileData>>> {
        let observable = Observable<Execution<BaseResponse<MyProfileData>>>.create { observer -> Disposable in
            // MARK: - UseCase 내부에서 Repository객체를 생성하여 참조하게 됨
            UserRepository().getMyProfile(accessToken: accessToken).subscribe (onNext: { result in
                switch result {
                case let .success(result):
                    observer.onNext(.success(result))
                    observer.onCompleted()
                case let .failure(error):
                    observer.onNext(.error(error.rawValue))
                    observer.onCompleted()
                }
            })
            .disposed(by: self.disposeBag)
            return Disposables.create()
        }
        return observable
    }
}

 

💉 의존성 주입 Dependency Injection

이렇게 의존하면 안되는데 객체를 참조해야 된다면, 객체 내부에서 참조해야될 객체를 생성하는 것이 아니라, 객체 외부에서 참조해야될 객체를 생성하여 해당 객체로 주입해주는 방법이 있다. 이게 바로 "의존성 주입" 이라는 개념이다.

 

UseCase와 Repository의 관계에 DI를 적용해보자면, 

 

1. Repository class를 구현할때, 레포지토리의 인터페이스를 정의하는 protocol을 만들고 해당 프로토콜을 준수하도록 구현한다. 

2. UseCase에서는 Repository의 protocol을 선언하고, Initializer 즉 생성자를 통해 외부에서 생성한 객체를 주입시킨다.

 

protocol UserRepositoryProtocol {
    func getMyProfile(accessToken: String) -> Observable<Result<BaseResponse<MyProfileData>, Error>>
}
struct UserRepository: UserRepositoryProtocol {
    func getMyProfile(accessToken: String) -> Observable<Result<BaseResponse<MyProfileData>, Error>> {
        return Observable.create { observer -> Disposable in
            AF.request(UserAPI.getMyProfile(accessToken: accessToken))
                .responseDecodable(of: BaseResponse<MyProfileData>.self) { response in
                    switch response.result {
                    case .success(let data):
                        observer.onNext(.success(data))
                    case .failure(let error):
                        observer.onNext(.failure(error))
                    }
                }
            
            return Disposables.create()
        }
    }
}
class UserUseCase {
    // MARK: - Repository protocol 선언
    private let userRepositoryProtocol: UserRepositoryProtocol

    // MARK: - 생성자를 통해 의존성 주입
    init(userRepositoryProtocol: UserRepositoryProtocol) {
        self.userRepositoryProtocol = userRepositoryProtocol
    }

    func getMyProfile(accessToken: String) -> Observable<Execution<BaseResponse<MyProfileData>>> {
        let observable = Observable<Execution<BaseResponse<MyProfileData>>>.create { observer -> Disposable in
            // MARK: - Repository 객체가 아니라 protocol에 접근
            self.userRepositoryProtocol.getMyProfile(accessToken: accessToken).subscribe (onNext: { result in
                switch result {
                case let .success(result):
                    observer.onNext(.success(result))
                    observer.onCompleted()
                case let .failure(error):
                    observer.onNext(.error(error))
                    observer.onCompleted()
                }
            })
            .disposed(by: self.disposeBag)
            return Disposables.create()
        }
        return observable
    }
}

 

 

ViewModel과 UseCase와의 관계도 동일하다.

 

Clean Architecture 관점에서만 봤을때는, Presentation Layer는 Domain Layer를 의존하고 반대로 Domain Layer는 Presentation Layer를 의존하면 안되니까 Presentation Layer에서 UseCase를 직접 생성하여 호출해도 된다고 생각할 수 있다. (사실 내가 그렇게 생각했다... ㅎㅎ)

 

하지만, MVVM 패턴이 적용된 Clean Architecture라서 ViewModel과 UseCase는 서로 의존하면 안된다.

따라서, ViewModel에서도 UseCase를 protocol로 선언하고 initializer를 통해 주입 받아서 사용해야한다.

 


 

혹시나 틀린 내용이나 저와 다른 생각이 있으시다면 댓글로 알려주세요~ :)

'iOS' 카테고리의 다른 글

[iOS] 딥링크(URI Scheme, Universal Link)와 Deferred DeepLink  (2) 2024.09.02