이전까지 글을 쓰고 글을 토대로 뷰와 Feature들에 대해 개발을 진행을 하였다. 규모가 점차 커졌고 나의 앱에서는 내가 여행중인 상태일때 coreLocation을 이용하여 특정한 위치에 갓을때 어떠한 이벤트를 주어야하기에 DI, DIP에 신경을 써야했다.
현재는 얼렁뚱땅 기존처럼 manager파일로 관리를 하였지만 TCA에서는 client라는 이름을 붙혀 외부 의존성을 추상화하고 관리하는 역할을 한다고 한다.
Dependency?
- 먼저 의존성이 뭔지를 알아야 한다 . 한 객체에서 다른 객체의 기능을 필요로 할때 -> 그 객체에 의존하고 있다.
ex) 어떠한 뷰모델에서 다른 뷰모델의 기능이 필요할때가 있다. 그래서 다른 뷰모델을 해당 뷰모델에서 생성하고 기능을 사용할 수 있다. 이를 의존하고 있다고 말한다..
.... 내가 두번째 프로젝트에서 주로 했던 짓이다...
그런데 문제점이 뭘까? 강한결합인경우 A가 B를 의존할때, B의 기능을 급하게 수정해야했다. 그러면 A도 같이 수정해야한다는 단점이 있다. 즉 유지보수성이 떨어지고 객체간의 독립성을 해친다. 그래서 의존성 주입에 신경을 써야한다.
DI(Dependency Injection)
의존성 주입은 객체가 필요한 의존성을 내부에서 생성하는 것이 아닌 외부에서 주입받는 패턴. 이를 통해 강한 결합을 느슨하게 만들어줄수 있다. 변경에 유연해지는 것이다.
1. 생성자 주입
class testViewModel: ObservableObject{
init(apiManager: NewApiManager, selectedVisibilityScope: VisibilityScopeType) {
self.apiManager = apiManager
self.selectedVisibilityScope = selectedVisibilityScope
fetchRecentSearch()
}
}
내가 주로 했던 방식으로 이런식으로 생성자 주입으로 의존성 필요할때 전달받아 사용하였다. testViewModel은 apiManager와 selec모시기에 의존하고 있다.
2. 혹은 속성을 주입하는 경우도 있다.
class SomeViewModel: ObservableObject {
var apiManager: NewApiManager?
var selectedVisibilityScope: VisibilityScopeType?
func setup(apiManager: NewApiManager, selectedVisibilityScope: VisibilityScopeType) {
self.apiManager = apiManager
self.selectedVisibilityScope = selectedVisibilityScope
}
}
생성자에서 주입하는 것이 아닌 객체가 생성된 후 외부에서 속성으로 주입하는 방식이다. 하지만 이 방식은 의존성이 즉시 주입되지 않고 어떠한 기능을 사용하면 오류가 생길수 있다는 위험이 존재하여 적절한 시점을 신경써야한다!!
하지만 여기서 끝난다? 그러면 엄청난 단점이 존재한다.
- 확장성이 부족하다: 구체적인 구현에 의존하고 있다. 만약 다른 네트워크 API관련 클래스가 필요한 경우 의존을 하고 있는 testViewModel의 코드를 수정해야한다.
- 테스트가 어렵: 테스트를 위해 목업 객체를 사용하기 어렵다. 예를들어 newAPIManager가 아니라 테스트환경의 MocKApiMangaer로 해야하는 경우 어떻게 할껀데
- 유지보수성 저하: 이건 확장성과 비슷할 수 있는데 apiManager가 바뀌었다? 그러면 testViewModel도 죄다 바꿔야한다.
DIP(Dependency Inversion Principle) 의존성 역전 원칙
- 위의 단점으로 적용하여야 하는 것이 의존성 역전 원칙이다. 구체적인 구현에 의존하지 말고, 추상화에 의존하도록 설계를 해야한다. 이를 통해 모듈간 결합도를 낮추고, 시스템을 유연하게 유지보수하고 확장할 수 있다.
위의 코드를 DIP를 적용한다면? 인터페이스를 적용하면 되는데 swift에는 프로토콜이 존재한다.
추가로 테스트용을 만들경우 MockApiManager: ApiManagerProtocol를 만들어 사용하면 된다.
// 추상화된 프로토콜
protocol ApiManagerProtocol {
func fetchData()
}
// 구체적인 구현체
class NewApiManager: ApiManagerProtocol {
func fetchData() {
// 실제 API 호출 로직
}
}
// ViewModel은 구체적인 클래스가 아닌 프로토콜에 의존함
class TestViewModel: ObservableObject {
let apiManager: ApiManagerProtocol
init(apiManager: ApiManagerProtocol) {
self.apiManager = apiManager
fetchRecentSearch()
}
}
@DependencyClient
- 자 DI, DIP에 대한 소개가 끝났다. 이제는 공부하고 있는 TCA로 돌아가서 보자. 위의 매크로는 TCA에서 제공하는 매크로로, 의존성 클라이언트를 쉽게 정의할 수 있게 해준다 왜? 프로토콜, 라이브 구현, 테스트용 구현을 자동으로 생성해주기에!
이렇게 되면 프로토콜처럼 어떠한 기능을 구현해야하는지 인터페이스를 구현해줄수 있다. 그런데 왜 굳이 protocol을 안사용하고 struct일까?
- 클로저 프로퍼티를 가질 수 있다.
- 값타입과 참조타입의 차이
@DependencyClient
struct LocationClient {
var requestauthorzizationStatus: @Sendable () async -> CLAuthorizationStatus?
var requestNotiAuthorization: () async throws-> Void
var startMonitoring: (Spot) async throws -> AsyncStream<MonitorEvent>
var stopMonitoring: (Spot) async -> Void
}
DependencyKey
- 의존성을 식별하고 기본값을 제공하는 프로토콜. LiveValue, TestValue, PreviewValue로 이전에 만든 객체를 넣어줘서 설정한다.
extension LocationClient: DependencyKey {
static let liveValue: Self = {
}}
DependencyValues
TCA에서 의존성을 주입하고 설정하는 역할. 마지막으로 DependencyValues를 확장하여 해당 객체에 대한 get, set 프로퍼티를 구현해주면된다. 이 확장을 통해 의존성의 생애 주기를 정의하고, 앱의 다른부분에서 쉽게 주입받아 사용할 수 있다.
extension DependencyValues {
var locationClient: LocationClient {
get { self[LocationClient.self] }
set { self[LocationClient.self] = newValue }
}
}
'SWIFTUI' 카테고리의 다른 글
ShareLink - 개발일기 (0) | 2024.11.09 |
---|---|
TCA- TestingCode (1) | 2024.10.26 |
TCA-3번째시간 Dependency (0) | 2024.07.19 |
TCA(2)-Store, ViewStore& Binding (4) | 2024.07.16 |
TCA(1)- (mvvm...-> NEXT?) (0) | 2024.07.04 |