이 튜토리얼에서, Swift의 의존성 주입(이하 DI) Framework중 하나인 Swinject를 통해 DI를 탐험할 것이다. Bitcoin의 현재 가격을 보여주는 "Bitcoin Adventurer"라는 iOS 앱을 개선하면 된다. 이 튜토리얼을 진행하면서 앱을 리팩토링하고, 도중에 유닛 테스트를 추가한다.
Dependency Injection(의존성 주입) 은 코드 자체의 종속성이 아닌, 다른 객체에 의해 제공되도록 코드를 구성하는 접근법이다. 이 방식으로 코드를 정렬하면 Test 및 Refactoring이 가능한 약하게 결합된 구성 요소(Components)의 코드베이스가 생성된다.
3rd Party Library 없이 DI를 구현할 수 있지만, Swinject는 DI 프레임워크에서 널리 사용되는 패턴인 DI Container를 사용한다. 이 패턴유형은 코드의 복잡성이 증가하더라도 종속성의 dependencies(해상도?) 를 단순하게 유지한다.
DI는 Inversion of Control (제어 반전) 이라는 원칙을 사용한다. 메인 아이디어는 일부 종속성을 요구하는 코드 조각이 자체적으로 종속성을 생성하는 것이 아니라 이러한 종속석을 제공하는 것에 대한 제어가 일부 높은 추상화로 지연된다는 것이다. 이러한 종속성은 일반적으로 object의 생성자로 전달된다. 이는 전형적인 객체 생성의 cascade(폭포수)에 대한 반대 접근 방식이다. 객체 A가 객체 B를 생성하고 객체 C를 생성하는 등의 작업을 수행한다.
현실적인 관점에서, 제어 반전의 주요 이점은 코드 변경 사항이 격리된 상태로 유지된다는 것이다. Dependency Injection Container 는 객체에 대한 종속성을 제공하는 방법을 알고 있는 객체를 제공하여 Inversion of Control 주체(principal) 를 지원한다.
Container에게 필요한 객체에 대해 물어보기만 하면 된다!
먼저 앱을 실행해보면, 비트코인의 현재 가격을 스크린에서 볼 수 있다.
Refresh
를 탭하면 최신 데이터를 검색하기 위한 HTTP 요청이 이루어지며, 이 요청은 Xcode 콘솔에 기록된다. 비트코인은 변동성이 큰 암호화폐로 가치가 자주 변동하기 때문에, Coinbase API는 약 30초마다 새로운 비트코인 가격을 이용할 수 있다.
Xcode 프로젝트로 돌아가 보면
BitcoinViewController.swift
에 있다. 현재 코드에는 뷰 계층이 기본 로직 및 종속성과 매우 밀접하게 결합되어 있기 때문에 UIViewController 생명주기와 독립적으로 로직을 테스트하기 어렵다.이전에는 종속성이 다른 객체에서 작업을 수행하는 데 필요한 코드 조각으로 정의 되었으며, 다른 객체에서 제공하거나 "주입"할 수 있는 코드 조각으로 정의되었다.
Bitcoin Adventurer 코드의 종속성에 대헤 알아보자.
BitcoinViewController.swift 코드는 세 개의 주요 역할이 있다.
대부분의 네트워킹은 requestPrice()
에서 일어난다.
private func requestPrice() {
let bitcoin = Coinbase.bitcoin.path
// 1. Make URL request
guard let url = URL(string: bitcoin) else { return }
var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringCacheData
// 2. Make networking request
let task = URLSession.shared.dataTask(with: request) { data, _, error in
// 3. Check for errors
if let error = error {
print("Error received requesting Bitcoin price: \(error.localizedDescription)")
return
}
// 4. Parse the returned information
let decoder = JSONDecoder()
guard let data = data,
let response = try? decoder.decode(PriceResponse.self,
from: data) else { return }
print("Price returned: \(response.data.amount)")
// 5. Update the UI with the parsed PriceResponse
DispatchQueue.main.async { [weak self] in
self?.updateLabel(price: response.data)
}
}
task.resume()
}
breakdonw:
URLRequest
를 만든다.request
를 사용하여 URLSessionDataTask
를 생성하고, task.resume()
을 호출하여 실행한다. 이것은 비트코인의 가격을 검색하기 위한 HTTP request를 실행한다.{
"data": {
"base": "BTC",
"currency": "USD",
"amount": "15840.01"
}
}
JSONDecoder
를 사용하여 JSON response를 PriceResponse
모델 객체에 매핑한다.updateLabel(price:)
로 전달되며, 이는 UI 업데이트가 main thread에서 수행되어야 하므로 main thread에 명시적으로 dispatch된다.BitcoinViewController
의 updateLabel(price:)
는 API에서 반횐되는 비트고인 가격이 달러와 센트로 올바르게 분할되어 표시 준비가 되도록 여러 개의 Formatter 객체를 사용한다.
private func updateLabel(price: Price) {
guard let dollars = price.components().dollars,
let cents = price.components().cents,
let dollarAmount = standardFormatter.number(from: dollars) else { return }
primary.text = dollarsDisplayFormatter.string(from: dollarAmount)
partial.text = ".\(cents)"
}
이는 단일 UIViewController에 강제적으로 적용되는 많은 로직이다.
Networking, Parsing, Formatting 기능은 여기에서 긴밀하게 결합된다.
전체 BitcoinViewController
객체와 독립적으로 테스트하거나 동일한 로직을 다른 곳에서 재사용하기는 어렵다.
이제 BitcoinViewController
를 리팩토링하여 networking 및 parsing 기능에 대해 별도의 객체를 만들것이다.
이 작업을 완료하면 Swinject를 사용하여 실제로 분리된 구성 요소(decoupled components)를 달성하기 위해 사용량(usage)을 조정(adjust)할 수 있다.