Swinject와 모듈, 의존성
개요
- 쏘카 라이브러리를 보다가
swinject라는 Library를 알게 되었다.
- 의존성 주입을 도와주는 Library라고 들었는데 의존성 주입 ( DI )에 대해서 알아보자.
의존성 주입
- 개발 시에 어떤 객체는 다른 클래스에 의존 하게 되는 상황이 발생 한다. 그 때 두 클래스는 서로 강하게 연결 되어 있게 되는데 그렇게 된다면 한 클래스의 코드가 변경 되면 그 클래스를 참고 하고 있는 다른 클래스에서도 영향을 받게 된다. 강하게 서로 의존 하고 있기 때문인데, swinJect Library를 통해 의존성을 대신 주입하게 되면 다른 객체를 통해서 연결 되기 때문에 강하게 의존하게 되는 일이 줄어들게 된다.
의존성 주입의 개념 비유
- 전구와 스위치
- 직접 연결된 경우 (높은 결합도)
스위치가 특정 전구 모델에만 작동하는 특별한 스위치라면, 그 전구가 고장나거나 새로운 기능의 전구로 교체하려고 할 때 문제가 생깁니다. 새로운 전구로 교체하려면 스위치도 함께 교체해야 합니다.
- 어댑터를 사용하는 경우 (의존성 주입)
전구와 스위치 사이에 다양한 전구에 맞게 연결해주는 "어댑터"를 끼워넣는다고 생각해보세요. 이제 스위치는 어댑터에 의존하게 되며, 어댑터를 통해 다양한 전구와 연결될 수 있습니다. 전구가 고장나거나 다른 모델로 교체해도 스위치나 어댑터를 교체할 필요가 없습니다. 스위치는 여전히 어댑터를 통해 전구를 제어할 수 있습니다.
코드를 통한 예시
class DatabaseService {
func fetchData() -> String {
return "Real Data from DB"
}
}
class UserService {
let dbService: DatabaseService
init() {
self.dbService = DatabaseService()
}
func getUserData() -> String {
return dbService.fetchData()
}
}
import Swinject
class DatabaseService {
func fetchData() -> String {
return "Real Data from DB"
}
}
class UserService {
let dbService: DatabaseService
init(dbService: DatabaseService) {
self.dbService = dbService
}
func getUserData() -> String {
return dbService.fetchData()
}
}
let container = Container()
container.register(DatabaseService.self) { _ in DatabaseService() }
container.register(UserService.self) { r in
let dbService = r.resolve(DatabaseService.self)!
return UserService(dbService: dbService)
}
let userService = container.resolve(UserService.self)!
import Swinject
struct User {
let id: Int
let name: String
}
protocol UserServiceProtocol {
func fetchUser() -> User
}
class UserService: UserServiceProtocol {
func fetchUser() -> User {
return User(id: 1, name: "John Doe")
}
}
class UserViewModel {
private let userService: UserServiceProtocol
var userName: String {
return userService.fetchUser().name
}
init(userService: UserServiceProtocol) {
self.userService = userService
}
}
let container = Container()
container.register(UserServiceProtocol.self) { _ in UserService() }
container.register(UserViewModel.self) { r in
let userService = r.resolve(UserServiceProtocol.self)!
return UserViewModel(userService: userService)
}
let userViewModel = container.resolve(UserViewModel.self)!
Swinject를 채택했을 때의 장점
- Swinject 사용 시의 장점
- 결합도 감소
- 객체나 클래스는 특정 구현에 직접적으로 의존하지 않는다. 이로 인해 코드의 유연성과 확장성이 향상된다.
- 테스트 용이성
- 실제 객체 대신 모의 객체나 스텁을 쉽게 주입하여 단위 테스트를 작성하고 실행할 수 있다.
- 유연성
- 필요에 따라 다른 구현을 쉽게 교체하거나 주입할 수 있다.
- 명확한 의존 관계
- 의존성이 명시적으로 주입되므로 코드의 의존 관계가 명확해진다.
- 설정과 코드의 분리
- 의존성 구성과 비즈니스 로직이 분리된다. 이로 인해 코드의 깔끔함과 유지 관리성이 향상된다.
- MVVM과의 통합
- ViewModel, Service, Repository 등의 의존성을 쉽게 관리하면서 MVVM 패턴의 테스트 용이성과 유연성을 높일 수 있다.
Swinject를 채택했을 때의 단점
- 학습 곡선
- Swinject나 의존성 주입에 익숙하지 않은 개발자는 초기에 학습하는 데 시간이 필요할 수 있다.
- 런타임 오류
- 컨테이너에서 의존성을 해결할 수 없는 경우 런타임에 오류가 발생할 수 있다.
- 성능 고려사항
- 대규모 애플리케이션에서 많은 의존성을 해결할 때 일부 성능 저하가 발생할 수 있다.
- 추가적인 라이브러리 의존성
- Swinject를 사용하면 프로젝트에 추가적인 외부 라이브러리 의존성이 생긴다.