본문 바로가기

iOS/Swift - UIKit

[Swift] URLSession(1) - 개념 모아보기(URLSession, URLSessionConfiguration, URLSessionTask)

✳️ URLSession 개요

📌 URLSession이란?

class URLSession : NSObject

An object that coordinates a group of related, network data transfer tasks.

네트워크 데이터 전달 작업에 연관된 일련의 일을 처리하는 그룹 오브젝트이다.

URLSession 클래스 및 관련 클래스들은 URLs에 의해 표현되는 엔드포인트에 데이터를 업로드하거나, 다운로드할 수 있도록 하는 API를 제공한다. 또한 URLSession을 통해 iOS 앱이 실행중이지 않을 때에도 백그라운드에서 데이터를 다운로드할 수 있다.

URLSessionDelegate나 URLSessionTaskDelegate를 사용하여 일의 완료(task completion)나 redirection과 같은 이벤트 또는 authentication(인증-로그인 또는 회원가입)을 지원할 수 있다.

본격적으로 URLSession에 대해 알아보기에 앞서, 우리는 URL Loading System에 대해 알 필요가 있다.

 

✨ URL Loading System

Interact with URLs and communicate with servers using standard Internet protocols.

표준 인터넷 프로토콜을 사용하여 서버와 통신하고 URLs와 상호작용하는 시스템.

URL 로딩 시스템은 https와 같은 표준 인터넷 프로토콜 또는 유저가 만든 커스텀 프로토콜을 사용하여, URLs에 의해 구분되는 자원들에 대해 접근할 수 있게 한다. 이러한 Loading은 비동기적으로 진행되기 때문에, 앱은 항상 반응성을 유지하여 들어오는 데이터나 에러가 도착할 때 이들을 통제할 수 있도록 한다.

 

이러한 URL Loading System을 가능하게 하는 것이 URLSession Class인 것이다.

URLSession 도식

 하나의 URLSession에서 여러 개의 URLSessionTask 인스턴스를 만들 수 있으며, URLSessionTask 각각은 데이터를 fetch하거나, 업로드하거나, 파일을 다운로드하는 역할을 할 수 있다.

 

⁉️ Session?

우리가 웹 브라우저를 사용할 때, 하나의 탭 또는 창마다 하나의 세션이 만들어지거나

상호작용의 역할을 하는 세션, 백그라운드 다운로드의 역할을 하는 세션으로 총 두 개의 세션이 만들어질 수 있다.

그리고 이러한 각각의 세션에 여러 개의 태스크를 추가하여 데이터를 다운로드하거나 업로드할 수 있는 것이다.

iOS 앱도 마찬가지로 생각하면 된다.

그리고 URLSessionConfiguration은 이러한 Session들의 기본적인 행동들을 결정하기 위해 사용된다.

예를 들어, 쿠키나 캐시를 어떻게 사용할지, 셀룰러 네트워크에 연결을 허락할지 등에 대한 결정을 할 수 있는 것이다. 그리고 이러한 세션의 설정은 각각의 세션이 가진 task들을 처리할 때 적용된다.

📌 URLSession을 구성하는 요소들

  • URLSessionConfiguration
  • URLSession Delegate
  • URLSession Task

📌 URLSession 사용 순서

  • URLSession Configuration 설정 및 Session 생성하기
  • Request 작성하기
  • Session에 Task 부여 및 실행하기

✳️ URLSession 사용해보기

1️⃣ URLSessionConfiguration 설정 및 Session 생성

let defaultSession = URLSession(configuration: .default)

위와 같이 간단한 방법으로 default configuration을 가진 Session 인스턴스를 생성할 수 있다.

또는 아래와 같이 URLSessionConfiguration을 인스턴스를 따로 생성하여 URLSession 생성 시에 사용할 수도 있다.

let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)

 

📌 URLSessionConfiguration의 타입

URLSession의 행동이나 정책을 결정하는 URLSessionConfiguration 클래스에는 세 가지 타입이 있다.

Default : 가장 기본적인 통신 방법. 이 방식을 사용할 일이 많다.

Ephemeral : 쿠키나 캐시, 인증을 저장하지 않을 때 사용한다. (ex. Safari의 개인정보보호 모드)

Background : 앱이 백그라운드에 있는 상황에서 HTTP와 HTTPs 방식의 컨텐츠 다운로드, 업로드를 할 때 사용한다.

 

📌 URLSessionConfiguration으로 설정할 수 있는 옵션

  • 기본적인 request와 response의 timeouts를 설정할 수 있다.
  • 네트워크에 연결될 때까지 기다리는 행동을 설정할 수 있다.
  • low data mode 또는 cellular connections를 이용할 때 앱이 네트워크를 사용하는 것을 막을 수 있다.
  • 모든 request에 추가적인 HTTP headers를 할당할 수 있다.
  • 임시 또는 백그라운드 세션을 생성할 수 있다.

 

📌 URLSession을 생성하지 않고 Singleton 객체를 사용하기

 URLSession은 보통 앱의 전역에서 사용되기 때문에 사용이 편하도록 싱글턴 세션도 제공한다. 싱글턴 세션은 URLSessionConfiguration을 가지고 있지 않기 때문에, configuration을 바꾸기 위해서는 다음과 같이 접근해야 한다.

// URLSession.shared.configuration.(컨피규레이션옵션) = (true,false 등등)
URLSession.shared.configuration.waitsForConnectivity = true
URLSession.shared.configuration.httpAdditionalHeaders = ["User-Agent": "MyApp 1.0"]

 이렇게 싱글턴 객체를 사용할 경우 세션을 따로 생성할 필요가 없다. 물론 원한다면 아래와 같이 싱글턴 객체를 참조하는 세션을 생성할 수 있다.

let session = URLSession.shared

2️⃣ URL 설정 및 URLReqest 생성

 URLRequest를 생성하기 위해서는 특정 URL 객체가 필요하다. URL을 결정하기 위해서는 URL 객체 또는 URLComponent를 이용할 수 있다.

📌 URL 객체 이용하여 기본적인 방식의 GET request 만들기

guard let url = URL(string: urlString) else { // API의 url을 넣어주면 된다
  print("URL is nil")
  return
}

// Request
let request = URLRequest(url: url)

위와 같이 특정 urlString에 대한 URL 인스턴스를 작성하고, URLRequest의 인자로 전달하여 request 인스턴스를 생성할 수 있다.

 

📌 URLComponent를 이용하여 query가 있는 GET 통신 준비하기

var urlComponents = URLComponents(string: "<https://leechamin.tistory.com/manage/posts?">)

// URLQueryItem 이용하여 조건(query) 생성
let userIDQuery = URLQueryItem(name: "userID", value: 1)
let recordIDQuery = URLQueryItem(name: "recordID", value: 124)

// 한국어를 넣어도 %EC%95%A0%ED%94%8C 처럼 자동으로 인코딩해서 입력해준다.
urlComponents?.queryItems?.append(userIDQuery)
urlComponents?.queryItems?.append(recordIDQuery)

// urlComponent의 url을 받아오기
guard let requestURL = urlComponents.url else { return }

 URLComponent는 URL과 마찬가지로 http String을 인자로 생성할 수 있다.

위의 예시처럼 URLComponent는 URLQueryItem을 가지기 때문에 쿼리를 관리할 때 편리하게 이용할 수 있다.

 

📌 map 함수를 이용하여 쿼리아이템 추가하기

 쿼리아이템을 딕셔너리 형태의 배열로 받아서 map 함수로 queryItemArray 배열에 매핑하고, 이를 URLComponents.queryItems에 할당할 수 있다.

func request(url: String, query: [String : String], completionHandler: @escaping (Bool, Any) -> Void) {

  ...

  // query 추가
  let queryItemArray = query.map {
      URLQueryItem(name: $0.key, value: $0.value)
  }
  urlComponents.queryItems = queryItemArray

  ...

}

 

📌 URLRequest 세팅하기

// httpMethod 설정하기, default는 "GET"으로 설정되어 있다.
request.httpMethod = "POST"
request.httpMethod = "GET"

// HTTPHeader 설정하기
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(accessToken, forHTTPHeaderField: "Authorization")

// httpBody 설정하기
let sendData = try! JSONSerialization.data(withJSONObject: param, options: [])
request.httpBody = sendData

 위와 같이 필요한 경우 httpMethod, HTTPHeader, httpBody를 설정할 수 있다. httpBody의 경우 POST나 PUT 통신에서 사용되며, httpMethod는 GET이 기본값이다.

HTTPHeader의 경우 request마다 설정해줄 수도 있지만, 액세스토큰의 존재를 확인해야하는 Authorization처럼 모든 task에 공통적으로 적용되어야 하는 경우 아래와 같이 URLSessionConfiguration에서 설정할 수도 있다.

URLSession.shared.configuration.httpAdditionalHeaders = ["Authorization": accessToken]

3️⃣ URLSessionTask 생성하기

지금까지 통신의 httpMethod와 query 또는 header를 결정하는 URLRequest 또는 URL을 만들었다. 다음 순서는 만들어진 URLRequest와 URL을 가지고 실질적인 통신을 하는 URLSessionDataTask를 만드는 것이다.

 

📌 URLSessionTask의 세 가지 종류

  • URLSessionDataTask: GET 요청을 위해 사용하는 Task로, 서버에서 메모리로 정보를 가져올 때 사용한다.
  • URLSessionUploadTask: POST 또는 PUT method를 통해 disk로부터 웹으로 파일을 업로드할 때 사용한다.
  • URLSessionDownloadTask: remote service로부터 일시적인 file location으로 파일을 다운로드할 때 사용한다.

적절한 task를 선택하여 생성한 다음, 완성된 task에 resume()메서드를 사용하면 서버통신이 시작된다.

 

📌 URLSessionDataTask 살펴보기

dataTask에는 아래와 같은 생성자들이 있다. URLRequest를 만들었는지, URL을 만들었는지, 컴플리션이 필요한지에 따라서 적절한 생성자를 골라서 사용할 수 있다.

URLSession.shared.dataTask(with: URLRequest, completionHandler: (Data?, URLResponse?, Error?) -> Void)
URLSession.shared.dataTask(with: URLRequest)
URLSession.shared.dataTask(with: URL)

 

📌 URL 객체로 request를 만든 경우의 GET 통신

// URL
guard let url = URL(string: urlString) else { // API의 url을 넣어주면 된다
  print("URL is nil")
  return
}

// Request
let request = URLRequest(url: url)

// Session
let defaultSession = URLSession(configuration: .default)

// Task
let task = defaultSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
  guard error == nil else {
    print("Error occur: error calling GET - \\(String(describing: error))")
    return
  }
  guard let data = data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
    print("Error: HTTP request failed")
    return
  }
  guard let output = try? JSONDecoder().decode(Model.self, from: data) else {
    print("Error: JSON data parsing failed")
    return
  }
}
// 통신 시작
task.resume()

 

📌 URLComponents를 사용한 경우(Query와 함께)의 GET 통신

아래를 보면 guard let 구문으로 urlComponents의 url을 가져오고 있다. 그리고 dataTask(with: URL) 생성자를 사용하여 url을 넣어주고 있다. else if let 구문을 사용한 것이 인상적이다.

// URLComponents
guard let urlComponents = URLComponents(string: "<https://itunes.apple.com/search>") else { return }
urlComponents.query = "media=music&entity=song&term=\\(searchTerm)"      

// URL
guard let url = urlComponents.url else {
  return
}

// dataTask declaration and resume
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in 
  if let error = error {
    self?.errorMessage += "DataTask error: " + error.localizedDescription + "\\n"
  } else if 
    let data = data,
    let response = response as? HTTPURLResponse,
    response.statusCode == 200 {       
    self?.updateSearchResults(data)

    DispatchQueue.main.async {
      completion(self?.tracks, self?.errorMessage ?? "")
    }
  }
}.resume() // task 실행

 

📌 request body를 가지는 POST 통신

아래를 보면 guard let 구문으로 urlComponents의 url을 가져오고 있다. 그리고 dataTask(with: URL) 생성자를 사용하여 url을 넣어주고 있다.

// requestBody encoding
let requestBody = try! JSONSerialization.data(withJSONObject: parameters, options: [])

// URL
guard let url = URL(string: url) else {
    print("Error: cannot create URL")
    return
}

// Request : 헤더, 바디 추가
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = requestBody

// Session
let defaultSession = URLSession(configuration: .default)

// Task
defaultSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
    guard error == nil else {
        print("Error occur: error calling POST - \\(String(describing: error))")
        return
    }

    guard let data = data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
        print("Error: HTTP request failed")
        return
    }

    guard let output = try? JSONDecoder().decode(Model.self, from: data) else {
        print("Error: JSON data parsing failed")
        return
    }

    completionHandler(true, output.data)
}.resume() // 통신 시작

나가며

위의 코드는 실전에서 사용보다는 개념 설명을 위한 코드이다. 예를 들어 ‘request body를 가지는 POST 통신'의 경우 위의 코드 전체를 메서드화해서 사용한다. URLSession은 앱 전역에서 사용할 일이 많기 때문에 공통화하여 사용할 때 더욱 효율적이다. 따라서 다음 글에서는 URLSession의 공통적인 부분을 모듈화해보고, 실전 통신에 사용할 수 있는 형태로 가공해보겠다.

 

레퍼런스

Apple Developer Documentation

URLSession Tutorial: Getting Started

URLSessionConfiguration Quick Guide

iOS) URLSession 에 대해서 알아보자(2/2) - 실전