의존성 주입(Dependency Injection)은 특정 아키텍처 패턴에 제한되지 않고, MVC를 포함한 대부분의 아키텍처 패턴에서도 충분히 활용할 수 있습니다. MVC에서도 의존성 주입을 사용하여 유연성, 테스트 용이성, 유지보수성을 개선할 수 있습니다.
MVC 패턴에서 의존성 주입을 사용하는 방법
MVC 패턴에서도 ViewController가 Model이나 Service 클래스와 독립적으로 작동하도록 의존성 주입을 활용할 수 있습니다. 보통 Controller에서 데이터 처리를 위해 Model이나 Service 객체가 필요할 때 의존성 주입을 사용해 외부에서 주입받는 방식으로 설정할 수 있습니다.
예를 들어, MVC에서 의존성 주입을 통해 UserService를 ViewController에 주입해보겠습니다.
1. UserService 정의
protocol UserServiceProtocol {
func getUserInfo() -> String
}
class UserService: UserServiceProtocol {
func getUserInfo() -> String {
return "User Info"
}
}
2. ViewController에서 UserService 의존성 주입
class UserViewController: UIViewController {
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol) {
self.userService = userService
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func displayUserInfo() {
let userInfo = userService.getUserInfo()
print(userInfo)
}
}
이제 UserViewController는 UserServiceProtocol에만 의존하게 되어, 실제 서비스와 테스트용 Mock 서비스 등 다양한 구현을 주입할 수 있습니다.
3. 실제 사용과 테스트 시의 주입
MVC 패턴에서도 사용 시와 테스트 시 각각 다른 객체를 주입할 수 있어 유연하게 코드를 사용할 수 있습니다.
// 실제 사용
let realUserService = UserService()
let viewController = UserViewController(userService: realUserService)
// 테스트 시
class MockUserService: UserServiceProtocol {
func getUserInfo() -> String {
return "Mock User Info"
}
}
let mockUserService = MockUserService()
let testViewController = UserViewController(userService: mockUserService)
요약
MVC에서도 의존성 주입을 활용하면, 유연하게 객체를 교체할 수 있어 테스트가 용이해지고, ViewController와 Model 사이의 결합도를 낮출 수 있습니다. 의존성 주입은 MVC를 포함한 다양한 아키텍처 패턴에서 적용 가능하며, 코드의 유지보수성과 테스트 가능성을 높이는 데 큰 도움이 됩니다.
🔥 실제 예시
이 코드에서 의존성이 있는 부분은 **네트워크 요청을 담당하는 URLSession.shared**입니다. 의존성 주입을 사용하면 네트워크 호출을 실제 URLSession이 아닌, 테스트나 개발 목적에 맞춘 Mock 객체로 대체할 수 있게 됩니다.
func getSpotImage(contentId: String, completion: @escaping (Result<AttractionImagesResponse, Error>) -> Void) {
var components = URLComponents(string: "\(Constants.base_URL)/detailImage1")
// 쿼리 아이템 설정
components?.queryItems = [
URLQueryItem(name: "serviceKey", value: Constants.api_key),
URLQueryItem(name: "MobileOS", value: "IOS"),
URLQueryItem(name: "MobileApp", value: "AppTest"),
URLQueryItem(name: "_type", value: "json"),
URLQueryItem(name: "contentId", value: contentId),
URLQueryItem(name: "imageYN", value: "Y"),
URLQueryItem(name: "subImageYN", value: "Y"),
URLQueryItem(name: "numOfRows", value: "10"),
URLQueryItem(name: "pageNo", value: "1")
]
// 퍼센트 인코딩 후 "+"를 "%2B"로 대체
if let encodedQuery = components?.percentEncodedQuery?.replacingOccurrences(of: "%25", with: "%") {
components?.percentEncodedQuery = encodedQuery
}
// URL 생성
guard let url = components?.url else { return }
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
guard let data = data, error == nil else {
if let error = error {
completion(.failure(error))
print("error 발생 1")
}
return
}
do {
let results = try JSONDecoder().decode(AttractionImagesResponse.self, from: data)
// 안전하게 items를 언래핑하고 nil일 경우 빈 배열 반환
completion(.success(results))
} catch {
completion(.failure(error))
print("error 발생 2")
}
}
task.resume()
}
1. URLSessionProtocol 정의
우선, URLSession의 동작을 추상화하기 위해 URLSessionProtocol 프로토콜을 정의합니다. 이를 통해 URLSession과 같은 네트워크 객체가 프로토콜을 준수하도록 만들 수 있습니다.
protocol URLSessionProtocol {
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
그리고 실제 URLSession이 URLSessionProtocol을 준수하도록 익스텐션을 추가합니다.
extension URLSession: URLSessionProtocol {}
2. 의존성 주입을 통한 네트워크 매니저 작성
이제 URLSessionProtocol을 이용해 네트워크 요청을 담당하는 NetworkManager를 작성하고, init 생성자를 통해 URLSessionProtocol 타입을 주입받도록 설정합니다.
class NetworkManager {
private let session: URLSessionProtocol
init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}
func getSpotImage(contentId: String, completion: @escaping (Result<AttractionImagesResponse, Error>) -> Void) {
var components = URLComponents(string: "\(Constants.base_URL)/detailImage1")
// 쿼리 아이템 설정
components?.queryItems = [
URLQueryItem(name: "serviceKey", value: Constants.api_key),
URLQueryItem(name: "MobileOS", value: "IOS"),
URLQueryItem(name: "MobileApp", value: "AppTest"),
URLQueryItem(name: "_type", value: "json"),
URLQueryItem(name: "contentId", value: contentId),
URLQueryItem(name: "imageYN", value: "Y"),
URLQueryItem(name: "subImageYN", value: "Y"),
URLQueryItem(name: "numOfRows", value: "10"),
URLQueryItem(name: "pageNo", value: "1")
]
// 퍼센트 인코딩 후 "+"를 "%2B"로 대체
if let encodedQuery = components?.percentEncodedQuery?.replacingOccurrences(of: "%25", with: "%") {
components?.percentEncodedQuery = encodedQuery
}
guard let url = components?.url else { return }
let task = session.dataTask(with: URLRequest(url: url)) { data, _, error in
guard let data = data, error == nil else {
if let error = error {
completion(.failure(error))
print("error 발생 1")
}
return
}
do {
let results = try JSONDecoder().decode(AttractionImagesResponse.self, from: data)
completion(.success(results))
} catch {
completion(.failure(error))
print("error 발생 2")
}
}
task.resume()
}
}
위 코드에서는 URLSession.shared 대신, URLSessionProtocol을 준수하는 임의의 객체가 session에 주입됩니다. 기본적으로는 URLSession.shared가 사용되지만, 테스트 시에는 다른 객체를 주입할 수 있습니다.
3. 테스트를 위한 Mock 객체 생성
테스트할 때는 실제 네트워크를 호출하지 않고, Mock 객체를 주입하여 원하는 응답을 테스트할 수 있습니다.
class MockURLSession: URLSessionProtocol {
var testData: Data?
var testError: Error?
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// CompletionHandler에 Mock 데이터와 Mock 에러를 전달
completionHandler(testData, nil, testError)
return URLSession.shared.dataTask(with: request) // 더미 URLSessionDataTask 반환
}
}
이제 MockURLSession을 사용하여 NetworkManager의 테스트를 진행할 수 있습니다.
4. 테스트 코드 작성
func testGetSpotImageWithMockData() {
let mockSession = MockURLSession()
mockSession.testData = /* 원하는 테스트 데이터 */
let networkManager = NetworkManager(session: mockSession)
networkManager.getSpotImage(contentId: "12345") { result in
switch result {
case .success(let response):
print("테스트 성공: \(response)")
case .failure(let error):
print("테스트 실패: \(error)")
}
}
}
이제 NetworkManager의 테스트 시 실제 네트워크 요청을 보내지 않고도 MockURLSession을 통해 원하는 테스트 데이터를 전달하여 검증할 수 있습니다.
요약
- **URLSessionProtocol**을 정의하여, URLSession을 추상화하고 의존성 주입할 수 있게 설정했습니다.
- NetworkManager가 생성자를 통해 URLSessionProtocol을 주입받도록 하여, 테스트 시에는 Mock 객체를 주입해 독립적인 테스트가 가능하게 만들었습니다
가령 AttractionImagesResponse의 구조가 아래와 같다고 가정해봅시다:
struct AttractionImagesResponse: Codable {
let images: [String]
}
그렇다면 mockSession.testData에 할당할 JSON 데이터는 다음과 같이 작성됩니다:
let mockDataString = """
{
"images": ["image1_url", "image2_url", "image3_url"]
}
"""
let mockData = mockDataString.data(using: .utf8)
mockSession.testData = mockData
이렇게 하면 네트워크 연결 없이도 NetworkManager.getSpotImage 메서드가 JSON 데이터를 디코딩하고 예상대로 작동하는지 테스트할 수 있습니다.
🔥 굳이 의존성 검사를 할 필요있나? 네트워크 통신 1번 해보면 되는데?
네트워크를 통해 실제로 데이터를 받아와서 동작 여부를 확인할 수도 있습니다. 그러나 의존성 주입을 사용해 Mock 데이터를 주입하는 방식은 몇 가지 중요한 이유로 권장됩니다.
1. 테스트의 신뢰성과 일관성
네트워크 기반 테스트는 외부 환경에 영향을 받기 때문에 일관성이 떨어질 수 있습니다. 예를 들어, 네트워크 상태나 서버 응답 속도, 서버의 가용성 등에 따라 테스트가 성공하거나 실패할 수 있습니다. 하지만 Mock 데이터를 사용한 테스트는 항상 동일한 조건에서 일관된 결과를 제공하므로, 테스트의 신뢰성과 일관성이 높아집니다.
2. 테스트 속도 향상
네트워크 요청을 실제로 보내고 응답을 받을 때까지 대기해야 하기 때문에, 네트워크 테스트는 일반적으로 속도가 느립니다. 반면, Mock 데이터를 사용하면 네트워크 대기 없이 즉시 데이터를 제공할 수 있어 테스트 속도가 빨라집니다. 테스트를 반복 실행할 경우, 이러한 차이는 개발 시간 단축에 크게 기여합니다.
3. 외부 시스템에 의존하지 않는 독립적인 테스트
네트워크를 통한 테스트는 외부 시스템에 의존합니다. 만약 서버가 다운되거나, API 응답 형식이 바뀌면, 개발자가 수정하지 않아도 코드가 동작하지 않거나 테스트가 실패할 수 있습니다. 반면, Mock 데이터를 사용한 테스트는 외부 시스템에 전혀 영향을 받지 않으므로 독립적인 테스트가 가능합니다.
4. 특정 조건 테스트 용이성
의존성 주입을 사용하면 네트워크 상태에 따라 성공, 실패 등 특정 시나리오를 쉽게 설정하고 테스트할 수 있습니다. 예를 들어, 에러가 발생했을 때의 상황을 확인하고 싶다면 Mock 데이터를 통해 에러 조건을 만들어 줄 수 있습니다. 네트워크 기반 테스트에서는 이러한 조건을 재현하기 어렵거나 번거로울 수 있습니다.
5. 유지보수와 코드의 유연성 향상
의존성 주입을 사용하면 코드의 결합도가 낮아지고, 유연성과 재사용성이 높아집니다. 만약 네트워크 로직을 테스트하는 대신 다른 데이터 소스(로컬 캐시 등)를 사용하도록 변경하려고 한다면, 의존성 주입을 통해 쉽게 교체할 수 있습니다. 코드의 구조가 더 모듈화되어 유지보수나 리팩토링에도 유리해집니다.
요약
- 일관성 있는 결과: 외부 네트워크 상태와 관계없이 일관된 테스트 결과를 얻을 수 있습니다.
- 빠른 테스트: 네트워크 대기 없이 빠르게 테스트할 수 있습니다.
- 외부 의존성 제거: 외부 시스템에 독립적이라서, 서버 상태에 관계없이 테스트가 가능합니다.
- 에러와 예외 상황 재현: 특정 시나리오를 쉽게 재현하여 테스트할 수 있습니다.
- 유지보수와 유연성: 코드 구조가 개선되어 재사용성과 유지보수성이 높아집니다.
'정보 > 레벨 1' 카테고리의 다른 글
Swift의 제네릭(Generic)에 대해 설명해주세요. (0) | 2024.11.15 |
---|---|
사용자 인터페이스(UI) 테스트와 단위(Unit) 테스트의 차이점은 무엇인가요? (2) | 2024.11.15 |
상속(Inheritance)과 프로토콜(Protocol)의 차이점은 무엇인가요? (1) | 2024.11.14 |
ARC(Automatic Reference Counting)의 동작 원리는 무엇인가요? (0) | 2024.11.14 |
UIKit에서 TableView와 CollectionView의 차이점은 무엇인가요? (0) | 2024.11.14 |