Swinject

velmash·2021년 5월 17일
0

이 튜토리얼에서, 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에게 필요한 객체에 대해 물어보기만 하면 된다!

Getting Stared

먼저 앱을 실행해보면, 비트코인의 현재 가격을 스크린에서 볼 수 있다.
Refresh를 탭하면 최신 데이터를 검색하기 위한 HTTP 요청이 이루어지며, 이 요청은 Xcode 콘솔에 기록된다. 비트코인은 변동성이 큰 암호화폐로 가치가 자주 변동하기 때문에, Coinbase API는 약 30초마다 새로운 비트코인 가격을 이용할 수 있다.

Xcode 프로젝트로 돌아가 보면

  • 이 앱에는 Main.storyboard 가 지원하는 단일 UIViewController, BitcoinController 가 포함되어 있다.
  • 모든 네트워킹 및 parsing 로직은 BitcoinViewController.swift에 있다. 현재 코드에는 뷰 계층이 기본 로직 및 종속성과 매우 밀접하게 결합되어 있기 때문에 UIViewController 생명주기와 독립적으로 로직을 테스트하기 어렵다.

DI and Coupling

이전에는 종속성이 다른 객체에서 작업을 수행하는 데 필요한 코드 조각으로 정의 되었으며, 다른 객체에서 제공하거나 "주입"할 수 있는 코드 조각으로 정의되었다.

Bitcoin Adventurer 코드의 종속성에 대헤 알아보자.

BitcoinViewController.swift 코드는 세 개의 주요 역할이 있다.

  • Networking
  • Parsing
  • Formatting

Networking and Parsing

대부분의 네트워킹은 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:

  1. 코인베이스 비트코인 현물 가격을 요청하는 URLRequest를 만든다.
  2. request를 사용하여 URLSessionDataTask를 생성하고, task.resume()을 호출하여 실행한다. 이것은 비트코인의 가격을 검색하기 위한 HTTP request를 실행한다.
    HTTP 요청이 성공하면, 아래와 같은 형식으로 JSON response를 return 한다.
{
  "data": {
    "base": "BTC",
    "currency": "USD",
    "amount": "15840.01"
  }
}
  1. response가 리턴되면 error를 검사하고, 오류가 있는 경우 print 한다.
  2. 에러가 없으면, JSONDecoder를 사용하여 JSON response를 PriceResponse 모델 객체에 매핑한다.
  3. 모델 객체는 updateLabel(price:)로 전달되며, 이는 UI 업데이트가 main thread에서 수행되어야 하므로 main thread에 명시적으로 dispatch된다.

Formatting

BitcoinViewControllerupdateLabel(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 객체와 독립적으로 테스트하거나 동일한 로직을 다른 곳에서 재사용하기는 어렵다.


밀접하게 결합된 구성 요소(components)의 대안으로는 서로 쉽게 연결 및 연결 해제될 수 있는 객체, 즉 느슨하게 결합된 객체를 만드는 것이 있다.

이제 BitcoinViewController를 리팩토링하여 networking 및 parsing 기능에 대해 별도의 객체를 만들것이다.
이 작업을 완료하면 Swinject를 사용하여 실제로 분리된 구성 요소(decoupled components)를 달성하기 위해 사용량(usage)을 조정(adjust)할 수 있다.

Extracting Dependencies

profile
https://github.com/velmash

0개의 댓글