본문 바로가기

학습 노트/iOS (2021)

209. URL Session Delegate

URL Session Delegate


Completion Handler가 Task가 완료되는 시점에 실행되는 것과는 다르게,
Session Delegate는 Task가 실행되는 동안 발생하는 모든 Event를 세부적으로 처리한다는 차이가 있다.
SessionDelegate의 구조는 다음과 같다.

Model.swift

struct Book: Codable {
	let id: Int
	let title: String
	let desc: String
	let link: String
	let date: Date
	
	enum CodingKeys: String, CodingKey {
		case id
		case title
		case desc = "description"
		case link = "yes24Link"
		case date = "publicationDate"
	}
}

서버에서 전달되는 데이터는 Book 구조체의 형식으로 저장한다.

 

Session 정의

SessionDelegateTableViewController.swift > sendRequest()

@IBAction func sendReqeust(_ sender: Any) {
    guard let url = URL(string: sampleUrl) else {
        fatalError("Invalid URL")
    }
      
    let configuration = URLSessionConfiguration.default
    session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

    let task = session.dataTask(with: url)

    task.resume()
}

SessionDelegate를 사용하기 위해서는 Session을 직접 생성하고 정의해야만 한다.
이를 정의하는 URLSession의 생성자의 파라미터는 다음과 같다.

  • URLSessionConfiguration
  • delegate
  • delegateQueue

이 중 URLSessionConfiguration은 Session 자체의 옵션을 결정하는 파라미터로 지금은 기본값을 사용하도록 구현했다.

 

SessionDelegate 구현

SessionDelegateTableViewController.swift > extension SessionDelegateTableViewController: UrlSessionDataDelegate

extension SessionDelegateTableViewController: URLSessionDataDelegate {
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
            completionHandler(.cancel)
            return
        }

        completionHandler(.allow)
    }
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        buffer?.append(data)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            showErrorAlert(with: error.localizedDescription)
        } else {
            parse()
        }
    }
}

 

urlSession(_:dataTask:didReceive:completionHandler:)

 

Apple Developer Documentation

 

developer.apple.com

 해당 메서드는 서버로부터 최초로 응답을 받았을 때 호출된다.
서버의 응답은 세번째 파라미터로 전달된다.

응답을 받았을때 취할 동작은 네 번째 파라미터인 CompletionHandler에서 정의한다.
응답을 확인하고 정상 범위인 200~299를 벗어난 응답 코드가 확인되면 Task를 취소한다.

 

urlSession(_:dataTask:didReceive:)

 

Apple Developer Documentation

 

developer.apple.com

해당 메서드는 서버에서 데이터가 전송될 때 마다 반복적으로 호출된다.
만약 100개의 데이터를 전송 받는다고 가정하면 최대 100번 호출된다.

서버에서 전달 된 데이터는 세 번째 파라미터로 전달된다.
전달된 데이터를 사용해 취할 동작은 마찬가지로 네 번째 파라미터인 CompletionHandler로 구현한다.
전달된 데이터를 미리 할당해 둔 Data 형식의 buffer 변수에 전달하도록 구현했다.

 

urlSession(_:task:didCompleteWithError:)

 

Apple Developer Documentation

 

developer.apple.com

해당 메서드는 서버에서의 데이터 전송이 완료된 다음에 호출된다.
발생하게 된 에러는 세번째 파라미터로 전달되고, 정상적으로 전송된 경우 nil을 저장한다.
보통은 해당 메서드를 사용해 에러 핸들링을 구현한다.

이번에도 세번째 파라미터를 확인해 에러를 확인해 이를 처리하고,
정상인 경우 parse 메서드를 호출하도록 구현했다.

SessionDelegateTableViewController.swift > extension SessionDelegateTableViewController

extension SessionDelegateTableViewController {
   func parse() {
       guard let data = buffer else {
           fatalError("Invalid Buffer")
       }

      let decoder = JSONDecoder()

      decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
         let container = try decoder.singleValueContainer()
         let dateStr = try container.decode(String.self)

         let formatter = ISO8601DateFormatter()
         formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]
         return formatter.date(from: dateStr)!
      })
   }
}

서버에서 정상적으로 전송된 데이터는 parse 메서드에서 다음 작업을 이어 나간다.
데이터가 저장 돼있는 buffer를 확인하고,
JSONDecoder를 설정한다.

 

데이터 표시

extension SessionDelegateTableViewController {
   func parse() {
       guard let data = buffer else {
           fatalError("Invalid Buffer")
       }

      let decoder = JSONDecoder()

      decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
         let container = try decoder.singleValueContainer()
         let dateStr = try container.decode(String.self)

         let formatter = ISO8601DateFormatter()
         formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]
         return formatter.date(from: dateStr)!
      })

       do {
           let detail = try decoder.decode(BookDetail.self, from: data)

           if detail.code == 200 {
               titleLabel.text = detail.book.title
               descLabel.text = detail.book.desc
               tableView.reloadData()
           } else {
               showErrorAlert(with: detail.message ?? "Error")
           }
       } catch {
           showErrorAlert(with: error.localizedDescription)
           print(error)
       }
   }
}

이후 decoder를 통해 변환된 데이터를 화면에 표시한다.

 

주의점

URLSession을 직접 정의, 생성하고, Delegate를 구현하는 것은
전송이 끝난 뒤 일괄적으로 처리하는 것이 아닌, 전송이 진행되는 것과 함께 처리하기 때문에
효율적이고 조금 더 복합적인 구현이 가능하지만 객체가 '강한 참조'로 연결된다는 문제가 생긴다.
이는 메모리 누수의 원인이며, 이를 막기 위해 session을 사용하고난 뒤에는 자원을 정리할 필요가 있다.

finishTasksAndInvalidate()

 

Apple Developer Documentation

 

developer.apple.com

invalidateAndCancel()

 

Apple Developer Documentation

 

developer.apple.com

위의 두 메서드가 그 역할을 한다.

finishTasksAndInvalidate()는 실행중인 Task가 모두 종료될 때까지 기다렸다가 메모리 할당을 해제한다.
반면 invalidateAndCancel()는 호출되는 즉시 모든 Task를 취소하고 메모리 할당을 해제한다.

위의 두 메서드를 호출하는 경우 기존에 사용하던 urlSession에 대한 메모리 할당을 해제하게 되므로 재사용이 불가능하다.
따라서 다시 Task를 사용하려면 새로운 urlSession을 생성해야 함을 명심하자.

 

결과


'학습 노트 > iOS (2021)' 카테고리의 다른 글

211. Post Request  (0) 2022.08.25
210. SessionConfiguration  (0) 2022.08.23
208. Data Task  (0) 2022.08.22
207. URL Loading System  (0) 2022.08.20
205~ 206. JSON  (0) 2022.07.13