Beginner question: why would I use this vs passing dependencies in initializers?
https://github.com/pointfreeco/swift-dependencies/discussions/228
Beginner question: why would I use this vs passing dependencies in initializers? · pointfreeco swift-dependencies · Discussion
Hello there, this is a question from a beginner, but I'm not sure why I would want to use this - and have my code needing to use a library, am I tying myself into the use of the library to structur...
github.com
위의 Community글을 가져온 것 입니다.(문제시 삭제하겠습니다.)
질문 해석:
"안녕하세요, 초보자의 질문이지만, 왜 이걸 사용해야 하는지 모르겠습니다. 라이브러리를 사용하게 되면 제 코드가 그 라이브러리에 종속되는 건가요? 제 코드를 구조화하기 위해 이 라이브러리에 얽매이게 되는 건 아닌가요?
초기화 메서드에서 의존성을 전달하는 방식이 더 직관적이라고 생각하는데, 굳이 라이브러리를 사용해서 얻는 이점이 무엇인지 잘 모르겠습니다. 초기화 메서드로 전달하면 어떤 코드가 어떤 의존성을 필요로 하는지 쉽게 볼 수 있지 않나요?
또한, A라는 기능이 B에 의존한다고 할 때, B가 A에게 필요한 것을 제공하지만 A는 그것을 어떻게 제공받는지 몰라야 하는 게 아닌가요?
이 질문이 좀 바보 같거나 이상하게 들리면 죄송합니다."
답변 해석:
안녕하세요, Pikuseru 님. 이 질문은 절대 바보 같지 않으며, 아마도 문서에서 더 잘 다룰 수 있는 내용일 겁니다. 물론 사람들이 읽을지는 모르겠지만요!
이 질문은 왜 의존성 관리 라이브러리를 사용해야 하는가에 대한 근본적인 이유와 관련이 있습니다. 단순히 이 라이브러리뿐만 아니라, 다른 어떤 라이브러리도요.
A라는 기능이 B라는 기능을 생성하는 경우를 고려했을 때는 의존성을 명시적으로 전달하는 것이 그렇게 큰 문제가 되지 않습니다. 진정한 이점은 수십 개, 아니 수백 개의 기능을 다룰 때 드러납니다.
예를 들어 기능 1에서 기능 2로, 기능 2에서 기능 3으로, 기능 3에서 기능 4로 이동할 수 있는 경우를 가정해 보겠습니다. 그리고 기능 4가 그 로직을 테스트 가능하게 유지하기 위해 날짜 생성기 의존성이 필요하다고 가정합니다. 그러면 기능 1~4는 모두 날짜 생성기가 필요하게 되지만, 실제로는 기능 4만 이 의존성이 필요한 것입니다.
struct Feature1 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
func goToFeature2() {
… = Feature2(date: date)
}
}
struct Feature2 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
func goToFeature3() {
… = Feature3(date: date)
}
}
struct Feature3 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
func goToFeature4() {
… = Feature4(date: date)
}
}
struct Feature4 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
}
기능 1~3이 날짜 의존성을 가져야 하는 이유가 기능 4에 의존하고 있고 기능 4가 날짜를 필요로 하기 때문이라고 주장할 수 있겠지만, 이로 인해 발생하는 보일러플레이트 코드를 고려하면 가치가 그리 크지 않을 수도 있습니다.
하지만, 리프(하위) Feature가 새로운 의존성, 예를 들어 API 클라이언트를 갑자기 필요로 하게 된다면 상황은 정말 복잡해집니다.
struct Feature4 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
}
이런 변화는 불행히도 애플리케이션 전체에 영향을 미쳐, 기능 4와 상호작용하는 모든 기능과 그 상호작용을 가지는 기능에 이르기까지 수정해야 하는 상황을 초래합니다.
struct Feature1 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
func goToFeature2() {
… = Feature2(
+ apiClient: apiClient,
date: date
)
}
}
struct Feature2 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
func goToFeature3() {
… = Feature3(
+ apiClient: apiClient,
date: date
)
}
}
struct Feature4 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
func goToFeature4() {
… = Feature4(
+ apiClient: apiClient,
date: date
)
}
}
이로 인해 기능에 의존성을 추가하는 것이 매우 어려워지며, 이러한 불편함 때문에 지름길을 택하게 되는 경우가 많습니다. 아마 가장 일반적인 지름길은 초기화 메서드에서 기본값을 설정해 주는 것입니다. 이렇게 하면 항상 의존성을 전달하지 않고도 Feature1(), Feature2() 등을 사용할 수 있고, 실제 의존성이 필요한 테스트에서는 Feature(apiClient: …, date: …) 형태로 적절한 테스트용 의존성을 제공할 수 있습니다.
init(
apiClient: APIClient = LiveAPIClient(),
@escaping date: () -> Date = { Date() }
) {
self.apiClient = apiClient
self.date = date
}
그러나 이 방식은 기능의 통합 테스트를 할 때 문제가 발생합니다. 이 패턴을 따르면, Feature1에 대한 테스트에서 Feature2의 로직을 실행하는 경우 Feature2는 실제 의존성을 사용하게 되며, 이로 인해 Feature1에서 의존성을 재정의했음에도 불구하고 실제 의존성으로 돌아가게 됩니다.
이와 비교하여 @Dependency를 사용하는 경우를 생각해 보세요. 기능은 독립적으로 필요한 의존성을 선언할 수 있습니다.
struct Feature4 {
@Dependency(\.apiClient) var apiClient
@Dependency(\.date) var date
…
}
그리고 기능이 의존성을 사용하지 않는 경우라면 이를 선언할 필요가 없습니다.
struct Feature1 {
// 필요하지 않은 경우 @Dependency 선언 없음
…
}
struct Feature2 {
// 필요하지 않은 경우 @Dependency 선언 없음
…
}
struct Feature3 {
// 필요하지 않은 경우 @Dependency 선언 없음
…
}
또한 리프 기능이 새로운 의존성을 필요로 하는 경우, 애플리케이션 내의 다른 기능을 전혀 수정하지 않고도 이를 추가할 수 있습니다.
struct Feature4 {
+ @Dependency(\.locationManager) var locationManager
…
}
그리고 테스트에서 기능이 @Dependency를 통해 접근하는 의존성을 테스트에서 재정의하지 않으면 테스트가 실패하게 되며, 이를 통해 테스트 시 사용하는 의존성을 명확히 정의하도록 강제할 수 있습니다.
사실 이 내용은 단지 표면적인 부분을 다룬 것에 불과합니다. 다양한 문서와 리포지토리에 있는 글을 읽어보길 추천드리며, 작년에 제가 이 주제에 대해 발표한 내용도 도움이 될 수 있을 것입니다.
글쓴이 질문에 대한 스스로의 부연 설명
테스트에서 Feature1에 종속성을 오버라이드했다고 가정해봅시다. 이 경우 Feature1 안에서 생성된 Feature2가 종속성을 명시적으로 전달받지 않거나 Feature1이 종속성을 Feature2에 전달하지 않으면, Feature2는 기본적으로 실제(live) 종속성을 사용하게 됩니다. 예를 들어 Feature1에 대해 APIClient 종속성을 모킹(mocking)했더라도, Feature2는 별도로 이 모킹된 종속성을 전달받지 않았다면 실제 APIClient를 참조하게 됩니다.
생각
swift-dependencies이후로 왜 API-Client 혹은 사용하는 객체를 함수형으로 짜야하는지에 대해 스스로 많은 생각을 했습니다. 초심자 입장에서 필요한 것들을 기본생성자로 주입하지 않고 Dependency 라이브러리를 쓰는 이유를 알기 쉽게 설명해준 것 같습니다.
'Swift > TCA' 카테고리의 다른 글
[TCA] Performance 읽어보기 (1) | 2024.08.29 |
---|---|
[TCA] Effect.swift 공식문서 음미하기 (0) | 2024.08.24 |
[TCA] day 13 타이머 (Effects- Timer) (0) | 2024.05.08 |
[TCA] day 12 새로고침 (Effects-Refreshable) (0) | 2024.05.07 |
[TCA] day 10 long living Effect (publisher Effect and async/await effect) (0) | 2024.05.05 |