iOS/Common

[Swift] 의존성 주입에 관하여(feat. Swinject, Dip, Typhoon)

TDCIAN 2023. 4. 10. 21:32

 

 

Q. Swift로 iOS 애플리케이션을 개발하는 과정에서 의존성 주입의 정의와 방법, 그리고 각 방법의 장단점에 대해 설명해 줘

 

A. 의존성 주입(Dependency Injection)은 객체 지향 프로그래밍(OOP)에서 사용되는 디자인 패턴 중 하나로, 클래스나 모듈 간의 의존성을 줄이고 유연성을 높이기 위해 사용됩니다.

의존성이란 객체 간의 상호작용을 의미하며, 한 객체가 다른 객체를 사용할 때 의존성이 생깁니다.

 

iOS 애플리케이션을 개발할 때도 여러 클래스나 모듈 간에 의존성이 생기는데, 이를 관리하기 위해 의존성 주입을 사용할 수 있습니다.

이를 통해 의존성을 외부에서 주입하여 클래스나 모듈을 유연하게 만들 수 있으며, 코드의 재사용성을 높일 수 있습니다.

 

의존성 주입 방법에는 크게 세 가지가 있습니다.

 

1. 생성자 주입(Constructor Injection)

- 생성자(Constructor)를 이용해 의존성을 주입하는 방법입니다.

- 클래스의 인스턴스를 생성할 때 생성자를 호출하면서 의존성을 주입합니다.

- 코드가 간결해지고 의존성을 명시적으로 나타낼 수 있으며, 의존성을 바꿀 때 클래스를 수정하지 않아도 됩니다.

- 하지만, 의존성이 많은 경우 생성자 파라미터가 많아져 코드가 지저분해질 수 있습니다.

 

+ Q. 의존성 주입 방법 중 생성자 주입 방식을 Swift 코드를 예시로 설명해 줘

 

A. 생성자 주입 방식은 클래스를 인스턴스화할 때 생성자 파라미터를 통해 의존성을 주입하는 방식입니다.

Swift에서는 이를 아래와 같이 구현할 수 있습니다.

class DataService {
    let networkService: NetworkService
    let storageService: StorageService
    
    init(networkService: NetworkService, storageService: StorageService) {
        self.networkService = networkService
        self.storageService = storageService
    }
    
    // ...
}

위 예시에서 DataService 클래스는 NetworkService와 StorageService에 의존하고 있습니다.

생성자에서 이 두 개의 의존성을 파라미터로 받아들이고, 클래스 내부에서 이를 저장합니다.

 

이제 DataService를 사용할 때는 이를 인스턴스화하고, 의존성을 주입해야 합니다.

이는 아래와 같이 수행할 수 있습니다.

let networkService = NetworkService()
let storageService = StorageService()

let dataService = DataService(
    networkService: networkService, 
    storageService: storageService
)

이렇게 인스턴스를 생성하면, 의존성이 주입된 DataService 인스턴스를 사용할 수 있게 됩니다.

이후에는 DataService가 제공하는 기능을 사용하면 되겠죠.

 

 

 

2. 프로퍼티 주입(Property Injection)

- 프로퍼티(Property)를 이용해 의존성을 주입하는 방법입니다.

- 클래스의 인스턴스를 생성한 후, 의존성을 주입하는 메서드를 호출하여 프로퍼티에 값을 할당합니다.

- 생성자 주입보다 유연성이 떨어지지만, 코드가 간결해지고 테스트하기 쉬워집니다.

 

+ Q. 의존성 주입 방법 중 프로퍼티 주입 방식을 Swift 코드를 예시로 설명해 줘

 

A. 프로퍼티 주입 방식은 클래스 인스턴스를 생성한 후에, 의존성을 설정하는 방식입니다.

클래스 내부에 의존성에 해당하는 프로퍼티를 선언하고, 인스턴스 생성 후에 이 프로퍼티에 값을 할당합니다.

Swift에서는 이를 아래와 같이 구현할 수 있습니다.

class DataService {
    var networkService: NetworkService!
    var storageService: StorageService!
    
    // ...
}

위 예시에서는 DataService 클래스 내부에 NetworkService와 StorageService에 해당하는 프로퍼티를 선언하고 있습니다.

이제 이 프로퍼티에 값을 할당하여 의존성을 주입할 수 있습니다.

let dataService = DataService()
dataService.networkService = NetworkService()
dataService.storageService = StorageService()

이렇게 인스턴스를 생성한 후, 프로퍼티에 값을 할당하여 의존성을 주입합니다.

이후에는 DataService가 제공하는 기능을 사용하면 됩니다.

 

프로퍼티 주입 방식의 장점은 코드가 간결하고, 테스트하기 쉽다는 점입니다.

하지만 유연성이 떨어지며, 생성자가 아닌 다른 메서드에서 의존성을 주입하기 어렵다는 단점이 있습니다.

 

 

 

3. 메서드 주입(Method Injection)

- 메서드(Method)를 이용해 의존성을 주입하는 방법입니다.

- 생성자와 마찬가지로 인스턴스를 생성할 때 메서드를 호출하면서 의존성을 주입합니다.

- 프로퍼티 주입보다 유연성이 높아지지만, 코드가 복잡해지고 테스트하기 어려워집니다.

 

+ Q. 의존성 주입 방법 중 메서드 주입 방식을 Swift 코드를 예시로 설명해 줘

 

A. 메서드 주입 방식은 의존성을 주입하는 메서드를 클래스 내부에 구현하고, 이를 외부에서 호출하여 의존성을 주입하는 방식입니다.

Swift에서는 이를 아래와 같이 구현할 수 있습니다.

class DataService {
    var networkService: NetworkService?
    var storageService: StorageService?
    
    func injectDependencies(
        networkService: NetworkService, 
        storageService: StorageService
    ) {
        self.networkService = networkService
        self.storageService = storageService
    }
    
    // ...
}

위 예시에서는 DataService 클래스 내부에 injectDependencies라는 메서드를 구현하고 있습니다.

이 메서드는 NetworkService와 StorageService에 해당하는 파라미터를 받아들여,

클래스 내부의 프로퍼티에 값을 할당합니다.

 

이제 DataService를 사용할 때는 인스턴스를 생성한 후, injectDependencies 메서드를 호출하여 의존성을 주입합니다.

let dataService = DataService()
dataService.injectDependencies(
    networkService: NetworkService(), 
    storageService: StorageService()
)

이렇게 메서드를 호출하여 의존성을 주입합니다.

메서드 주입 방식은 의존성을 다른 메서드에 주입할 수 있다는 장점이 있습니다.

하지만 이 방식 역시 프로퍼티 주입 방식과 마찬가지로 유연성이 떨어진다는 단점이 있습니다.

 

+ Q. iOS 개발 과정에서의 의존성 주입 방식 중 프로퍼티 주입 방식과 메서드 주입 방식이 유연성이 떨어진다고 했는데, 이게 무슨 의미인지 Swift 코드를 예시로 설명해 줘

 

A. 프로퍼티 주입 방식과 메서드 주입 방식은 의존성 주입이 한 번 이루어진 이후, 런타임에 변경하기 어려운 단점이 있습니다.

이를 예시를 통해 설명해보겠습니다.

class WeatherService: WeatherServiceProtocol {
    var weatherAPI: WeatherAPIProtocol
    
    init(weatherAPI: WeatherAPIProtocol) {
        self.weatherAPI = weatherAPI
    }
    
    func getWeatherForecast() -> [String] {
        let weatherData = weatherAPI.fetchWeatherData()
        // ...
    }
}

let weatherAPI = WeatherAPI()
let weatherService = WeatherService(weatherAPI: weatherAPI)
let weatherViewModel = WeatherViewModel(weatherService: weatherService)

위 코드에서 'WeatherViewModel' 클래스는 'WeatherService' 객체를 생성자 주입 방식을 통해 받습니다.

이때 'WeatherService' 클래스는 'WeatherAPIProtocol' 프로토콜을 따르는 'weatherAPI' 프로퍼티를 가지고 있습니다.

이때 'WeatherAPI' 클래스는 'WeatherAPIProtocol' 프로토콜을 구현하고 있습니다.

 

하지만 만약 추후에 'WeatherAPI' 클래스를 업데이트하여 'WeatherAPIProtocol' 프로토콜을 구현하지 않는다면,

'WeatherService' 클래스에서는 컴파일 오류가 발생할 것입니다.

또한, 이미 생성된 'WeatherService' 객체의 'weatherAPI' 프로퍼티를 변경하는 것은 불가능합니다.

 

이와 같이 프로퍼티 주입 방식과 메서드 주입 방식은 객체의 생성 시점에서 의존성 주입이 이루어지기 때문에,

런타임에 의존성을 변경하는 것이 어렵습니다. 따라서 유연성이 떨어질 수 있다는 단점이 있습니다.

 

 

 

각 방법의 장단점을 정리하면 다음과 같습니다.

 

생성자 주입(Constructor Injection)

- 장점: 코드가 간결해지며, 의존성을 명시적으로 나타낼 수 있습니다.

- 단점: 의존성이 많은 경우 생성자 파라미터가 많아져 코드가 지저분해질 수 있습니다.

 

프로퍼티 주입(Property Injection)

- 장점: 코드가 간결해지며, 테스트하기 쉽습니다.

- 단점: 유연성이 떨어지며, 생성자가 아닌 다른 메서드에서 의존성을 주입하기 어렵습니다.

 

+ Q. iOS 개발 과정에서의 의존성 주입 방식 중 프로퍼티 주입 방식이 테스트하기 쉽다고 하던데, 테스트 코드의 예시를 Swift로 설명해 줘.

 

A. 프로퍼티 주입 방식은 테스트 코드에서 매우 간편하게 모의 객체(Mock Object)를 사용하여 테스트할 수 있습니다.

이를 통해 의존성이 있는 객체를 모의 객체로 대체하여 테스트를 수행할 수 있습니다.

아래는 Swift로 작성된 테스트 코드의 예시입니다.

// 모의 객체 정의
class MockWeatherService: WeatherServiceProtocol {
    var forecast: [String] = []
    func getWeatherForecast() -> [String] {
        return forecast
    }
}

// 테스트 코드
class WeatherViewModelTests: XCTestCase {
    var viewModel: WeatherViewModel!
    var mockService: MockWeatherService!

    override func setUp() {
        super.setUp()
        mockService = MockWeatherService()
        viewModel = WeatherViewModel()
        viewModel.weatherService = mockService
    }

    func testFetchWeatherForecast() {
        // 모의 객체에 반환값 설정
        mockService.forecast = ["Sunny", "Cloudy", "Rainy"]

        // 뷰모델에 있는 함수 호출
        viewModel.fetchWeatherForecast()

        // 기대값과 실제값 비교
        XCTAssertEqual(viewModel.numberOfRows(), 3)
        XCTAssertEqual(viewModel.titleForRow(0), "Sunny")
        XCTAssertEqual(viewModel.titleForRow(1), "Cloudy")
        XCTAssertEqual(viewModel.titleForRow(2), "Rainy")
    }
}

위 코드에서 'MockWeatherService' 클래스는 'WeatherServiceProtocol' 프로토콜을 따르면서, 

'getWeatherForecase()' 메서드 대신 'forecast' 프로퍼티를 이용하여 값을 반환합니다.

'WeatherViewModel' 클래스에서 'weatherService' 프로퍼티에 'MockWeatherService' 객체를 할당하고,

이를 사용하여 'fetchWeatherForecast()' 함수를 호출합니다. 그리고 반환된 값과 기대값을 비교하여 테스트를 수행합니다.

 

이와 같이 프로퍼티 주입 방식은 의존성이 있는 객체를 대체하여 테스트를 수행할 수 있기 때문에, 테스트 코드 작성이 매우 용이합니다.

 

메서드 주입(Method Injection)

- 장점: 생성자나 프로퍼티보다 유연성이 높습니다.

- 단점: 코드가 복잡해지며, 테스트하기 어렵습니다.

 

iOS 애플리케이션을 개발할 때는 보통 생성자 주입 방법이 가장 많이 사용됩니다.

이는 코드가 간결하고, 의존성을 명시적으로 나타낼 수 있기 때문입니다.

하지만, 의존성이 많아질 경우 생성자 파라미터가 많아져 코드가 지저분해질 수 있으니,

이런 경우에는 프로퍼티 주입이나 메서드 주입을 사용할 수도 있습니다.

 

의존성 주입은 iOS 애플리케이션 개발에 있어서 중요한 개념 중 하나입니다.

이를 통해 코드의 유연성과 재사용성을 높일 수 있으며, 유지보수하기 쉬운 코드를 작성할 수 있습니다.

따라서, iOS 개발자라면 의존성 주입에 대한 이해와 활용 방법에 대해 꼭 알고 있어야 합니다.

 

 

 

Q. iOS 애플리케이션 개발 간 의존성 주입을 좀 더 편리하게 할 수 있도록 도와주는 라이브러리를 추천해 줘

 

A. iOS 애플리케이션에서 의존성 주입을 쉽고 간편하게 사용할 수 있는 다양한 라이브러리들이 있습니다.

그중에서도 가장 널리 사용되는 라이브러리는 다음과 같습니다.

 

1. Swinject

 

Swinject는 Swift로 작성된 경량의 의존성 주입 프레임워크입니다.

기본적으로 생성자 주입 방식을 지원하며, 스위프트의 제네릭과 프로토콜 기능을 사용하여 의존성 주입을 처리할 수 있습니다.

Swinject는 많은 개발자들에게 인기가 있는 라이브러리 중 하나이며, 많은 문서와 튜토리얼이 제공되고 있어, 초기 학습이 쉽습니다.

 

+ Q. Swinject 라이브러리를 활용하는 예시를 코드로 설명해 줘

 

A. Swinject는 Swift로 작성된 경량의 의존성 주입 프레임워크이며, 간단한 예시를 통해 사용 방법을 설명해 드리겠습니다.

다음과 같은 간단한 예시를 고려해 보겠습니다. UserService라는 클래스가 있다고 가정하겠습니다.

UserService 클래스는 User 클래스에 의존하며, 생성자 주입 방식을 사용하여 의존성을 주입받습니다.

class User {
    let name: String
    let age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class UserService {
    let user: User
    
    init(user: User) {
        self.user = user
    }
}

이제 Swinject를 사용하여 UserService 클래스의 의존성을 주입하는 코드를 작성해 보겠습니다.

먼저, Swinject를 설치하고, 다음과 같이 Swinject 컨테이너를 생성합니다.

import Swinject

let container = Container()

다음으로, UserService 클래스와 User 클래스를 등록합니다.

container.register(User.self) { _ in
    User(name: "John Doe", age: 30)
}

container.register(UserService.self) { r in
    UserService(user: r.resolve(User.self)!)
}

위의 코드에서 'register' 메서드를 사용하여 UserService 클래스와 User 클래스를 등록합니다.

먼저, User 클래스를 등록하며, 클로저를 사용하여 User 클래스의 인스턴스를 생성합니다.

UserService 클래스를 등록할 때, 'resolve' 메서드를 사용하여 User 클래스의 인스턴스를 가져옵니다.

이후 UserService 클래스의 인스턴스를 생성하며, 생성자에 User 클래스의 인스턴스를 주입합니다.

 

마지막으로, UserService 클래스의 인스턴스를 가져오는 코드를 작성합니다.

let userService = container.resolve(UserService.self)!

위의 코드에서 'resolve' 메서드를 사용하여 UserService 클래스의 인스턴스를 가져옵니다.

이때, Swinject는 UserService 클래스의 생성자를 호출하여 User 클래스의 인스턴스를 주입합니다.

 

이렇게 Swinject를 사용하여 의존성을 주입할 수 있습니다.

Swinject는 다양한 방식의 의존성 주입을 지원하며, 코드의 복잡도를 줄여주는 기능들을 제공합니다.

또한 Swinject는 스위프트의 제네릭과 프로토콜 기능을 사용하여 의존성 주입을 처리할 수 있어, 다양한 상황에 대응할 수 있습니다.

 

 

2. Dip

 

Dip는 의존성 주입을 보다 쉽게 구현할 수 있도록 도와주는 경량의 Swift 라이브러리입니다.

Dip는 생성자 주입 방식을 지원하며, 스위프트의 제네릭 기능을 활용하여 의존성 주입을 처리합니다.

또한 다양한 유형의 객체와 값, 그리고 프로토콜을 지원합니다.

 

+ Q. Dip 라이브러리를 활용하는 예시를 코드로 설명해 줘

 

A. Dip는 의존성 주입을 위한 Swift 프레임워크로, 스위프트의 제네릭과 프로토콜 기능을 사용하여 의존성 주입을 처리합니다.

Dip를 사용하여 UserService 클래스의 의존성을 주입하는 코드를 작성해 보겠습니다.

 

먼저, UserService 클래스와 User 클래스를 정의합니다.

class User {
    let name: String
    let age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class UserService {
    let user: User
    
    init(user: User) {
        self.user = user
    }
}

다음으로, Dip을 사용하여 UserService 클래스의 의존성을 주입하는 코드를 작성합니다.

import Dip

let container = DependencyContainer()

try! container.register { User(name: "John Doe", age: 30) }
try! container.register { UserService(user: $0) }

위의 코드에서 'DependencyContainer'를 사용하여 Dip 컨테이너를 생성합니다.

'register' 메서드를 사용하여 User 클래스와 UserService 클래스를 등록합니다.

User 클래스는 클로저를 사용하여 인스턴스를 생성합니다.

UserService 클래스는 '$0'을 사용하여 User 클래스의 인스턴스를 주입합니다.

 

마지막으로, UserService 클래스의 인스턴스를 가져오는 코드를 작성합니다.

let userService = try! container.resolve() as UserService

위의 코드에서 'resolve' 메서드를 사용하여 UserService 클래스의 인스턴스를 가져옵니다.

이 때, Dip는 UserService 클래스의 생성자를 호출하여 User 클래스의 인스턴스를 주입합니다.

 

Dip은 Swinject와 마찬가지로 다양한 방식의 의존성 주입을 지원합니다.

Dip은 Swinject와 달리, 선언적인 방식으로 의존성 주입을 처리할 수 있어, 코드의 가독성을 높일 수 있습니다.

또한, Dip은 컴파일 타임에 의존성을 검증할 수 있어, 런타임 에러를 방지할 수 있습니다.

 

 

 

3. Typhoon

 

Typhoon은 Objective-C와 Swift 모두에서 사용할 수 있는 의존성 주입 프레임워크입니다.

Typhoon은 생성자 주입 방식뿐만 아니라 프로퍼티와 메서드 주입 방식도 지원하며, 런타임에 의존성을 주입하는 방식을 사용합니다.

또한 Typhoon은 의존성 그래프를 시각화할 수 있는 기능을 제공하여 개발자들이 의존성 구조를 이해하는 데 도움이 됩니다.

 

+ Q. Typhoon 라이브러리를 활용하는 예시를 코드로 설명해 줘

 

A. Typhoon은 Objective-C와 Swift에서 사용 가능한 의존성 주입 프레임워크입니다.

이번에는 Swift에서 Typhoon을 사용하여 UserService 클래스의 의존성을 주입하는 코드를 작성해 보겠습니다.

 

먼저, UserService 클래스와 User 클래스를 정의합니다.

class User {
    let name: String
    let age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class UserService {
    let user: User
    
    init(user: User) {
        self.user = user
    }
}

다음으로, Typhoon을 사용하여 UserService 클래스의 의존성을 주입하는 코드를 작성합니다.

import Typhoon

class Assembly: TyphoonAssembly {
    func user() -> AnyObject {
        return TyphoonDefinition.with { definition in
            definition.useInitializer(User.init(name:age:))
            definition.injectProperty(#keyPath(User.name), with: "John Doe")
            definition.injectProperty(#keyPath(User.age), with: 30)
        }
    }
    
    func userService() -> AnyObject {
        return TyphoonDefinition.with { definition in
            definition.useInitializer(UserService.init(user:))
            definition.injectProperty(#keyPath(UserService.user), with: self.user())
        }
    }
}

let assembly = Assembly()

let userService = assembly.userService() as! UserService

위의 코드에서 'Assembly' 클래스를 정의합니다. 

'Assembly' 클래스는 'TyphoonAssembly' 프로토콜을 채택합니다.

'user' 메서드와 'userService' 메서드를 정의하여 User 클래스와 UserService 클래스의 의존성을 주입합니다.

'user' 메서드에서는 User 클래스의 인스턴스를 생성하고,

'userService' 메서드에서는 UserService 클래스의 인스턴스를 생성합니다.

이때, UserService 클래스의 'user' 프로퍼티에 User 클래스의 인스턴스를 주입합니다.

 

마지막으로, 'userService' 메서드를 호출하여 UserService 클래스의 인스턴스를 가져옵니다.

Typhoon은 스토리보드와 함께 사용 가능한 등록식 의존성 주입 및 인스턴스 생성을 제공합니다.

또한, Typhoon은 Objective-C와 Swift 모두에서 사용 가능하며, 타입 세이프하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

 

 

출처: ChatGPT(https://chat.openai.com/chat)