Search

[정리] URLSession 뜯어보기 - Shared Session 사용해서 데이터 보내고 받아보기

 들어가며

이번 포스팅에서는 URLSession를 갈기갈기 뜯어보려고 합니다.
URLSession이 뭔지, 그리고 어떤 속성들을 네트워크 코드를 만들기 위해서 사용하는지 먼저 알아보고자 합니다. 그리고나서 네트워크 예제 코드를 직접 만들어서 함께 보도록 하겠습니다.

 URLSession

공식 문서에서 URLSession를 검색하면 이렇게 설명합니다.
An object that coordinates a group of related, network data transfer tasks. 관련 네트워크 데이터 전송 작업 그룹을 조정하는 개체
앱과 서버 간에 데이터를 주고받기 위해서 사용하는 개체라고 생각해주시면 됩니다.
URLSession 클래스에서는 URL로 표시된 EndPoint에서 데이터를 다운로드하고 업로드하기 위한 API를 제공해줍니다.
해당 API는 앱이 실행 중일 때 뿐만 아니라 실행되지 않고 있거나 일시 중단된 상태일 때도 Background에서 다운로드를 수행할 수 있도록 해줍니다.
URLSession를 사용하면 URLSessionDelegate, URLSessionTaskDelegate 같은 Delegate를 통해서 Authentication, Redirect 및 Task 완료와 같은 네트워크 이벤트를 수신받을 수도 있습니다.
앱에서는 하나 이상의 URLSession 인스턴스를 생성할 수 있습니다. 하지만 Session를 필요 이상으로 만들지 않도록 주의해야 합니다. 유사하게 구성된 Session이 앱 여러 부분에 있으면 이를 하나의 Session으로 만들어서 공유하는 것이 좋습니다.

Type of URLSession

URLSession에는 Session Configuration에 따라서 3가지 종류가 존재합니다. ⓵ default, ⓶ ephemeral, ⓷ background session 입니다.
하지만, 3가지 URLSession 타입들을 알아보기 전에 Shared Session 부터 알아봅시다.
Shared Session은 URLSession의 Shared Singleton Object 입니다. Configuration에 영향을 받지 않으면서 기본적으로 많이 사용하기 때문에 URLSession를 사용해서 네트워크 연결을 해 본 경험이 있다면 보셨을겁니다.
원래 URLSession 인스턴스를 생성할 때에 URLSessionConfiguration 개체를 넣어서 생성하지만, Shared Object는 Configuration 없이도 기본적인 네트워크를 진행할 수 있도록 도와줍니다.
아래 코드를 보시면 URLSession를 URLSessionConfiguration를 사용해서 생성하지 않고 shared 속성을 직접 사용해서 네트워크 코드를 짰습니다.
let request = URLRequest(url: URL(string: "https://www.naver.com")!) let task = URLSession.shared.dataTask(with: request) task.resume()
Swift
복사
URLSession를 쉽게 생성할 수 있는 Shared가 있는데 URLSessionConfiguration이 굳이 필요있나요?
라고 생각하실 수도 있습니다. 저도 그렇게 생각했었습니다. 여태껏 shared 로 네트워크 연결을 했지만 단 한 번도 큰 문제가 발생했던 적은 없거든요. 그렇다면 shared가 어떤 한계점을 가져서 URLSessionConfiguration를 써야만 하는 경우들이 생기는 걸까요?

Shared Session

Shared Session은 기본 요청을 위한 개체입니다. URLSession 클래스는 Shared Session에 Task를 생성하기 위한 합리적인 기본 동작을 제공해줍니다.
다른 Session 타입들과는 다르게 Shared Session은 만들지 않아도 됩니다. delegate, configuration를 따로 커스텀해서 제공하지 않아도 충분히 우리가 만들고 싶은 네트워크 코드를 구성할 수 있습니다. 따라서, 요구 사항이 제한적인 경우에 사용하기 좋습니다.
shared 하나만 사용하면 코드 몇 줄만으로 URL의 내용을 메모리로 가져올 수 있기 때문이죠.
그렇기 때문에, 제약이 존재합니다.
shared는 delegate, configuration object를 가지지 않습니다. 따라서 default connect behavior를 사용자 지정할 수 없습니다. 설정해놓은 대로 사용해야 합니다.
Authentication 기능 제한
Cache, Cookie Storage, Credential Storage 커스텀 불가
앱이 실행되고 있지 않을 때 Background 다운로드, 업로드 수행 불가
이러한 제약 사항들이 존재하기 때문에 사용자 지정 Configuration를 설정해서 Session를 만들 수 있도록 한겁니다. 그렇다면 Session 타입들에 대해서 알아보도록 합시다.

.default Session

.default Session은 추가로 사용자 지정하지 않은 한 shared Session과 비슷하게 동작합니다.
shared와는 다르게 delegate를 할당해서 데이터를 가져올 수 있습니다.

.ephemeral Session

.ephemeral Session은 .default Session과 비슷하지만 cache, cookies, credential를 디스크에 쓰지 않습니다.

.background Session

.background Session은 앱이 실행되지 않는 동안 Background에서 콘텐츠 업로드, 다운로드를 수행합니다.
Session 타입들은 특징이 있습니다. 각 특징에 맞춰서 Session를 구성할 때 URLSessionConfiguration Object로 설정해주면 됩니다.
계속 Configuration, Configuration 하면서 정작 URLSessionConfiguration은 제대로 소개를 못했네요. 이름 그대로 URLSession의 Configuration를 설정하는 개체입니다.

URLSessionConfiguration

URLSessionConfiguration은 Session의 동작과 정책을 정의하는 개체입니다. 즉, Session의 룰을 정의해둔 개체이기 때문에 Session 생성에 꼭 필요한 개체입니다.
하지만, shared 같은 경우는 기본 Configuration를 사용하기 때문에 Session 생성 시에 URLSessionConfiguration를 따로 넣어주지 않아도 괜찮은 겁니다.
Session 개체 초기화 전에 URLSessionConfiguration 개체를 구성하는 것이 중요합니다. 사용자가 제공한 Configuration의 복사본을 만들어서 해당 Configuration를 사용해서 세션을 구성하게 됩니다.
URLSessionConfiguration를 사용하게 되면 아래의 값들을 구성할 수 있습니다:
timeout 값
caching 정책
connection requirement
URLSession에 사용하려는 기타 유형 정보들
어떤 상황에서는 Configuration에 의해서 정의된 Policy가 NSURLRequest 에 의해서 재정의될 수 있습니다. Request 개체에 지정된 모든 Policy는 Session Policy가 더 제한적인 경우에 적용됩니다.
예를 들어서, Configuration에서 셀룰러 네트워킹을 허용되지 않도록 하면, NSURLRequest는 셀룰러 네트워킹을 요청할 수 없게 됩니다.
우리가 URLSessionConfiguration으로 URLSession를 생성하고 나면 이제 Task를 생성해서 네트워킹을 진행해야 합니다. 그렇다면, Task는 또 무엇일까요?

URLSessionTask

Task는 말그대로 작업입니다. URLSession 인스턴스에서 Task 생성 방법 중 하나를 호출해서 Task를 생성하기 때문에 각 작업들은 URLSession 안에 속하게 됩니다.
작업 유형은 호출 방법에 따라서 결정됩니다.

URLSessionDataTask

서버에 짧은 interactive 요청을 하는 경우 사용합니다. 기본적인 데이터를 받을 때 사용한다고 보시면 됩니다. dataTask는 리소스 요청을 하고 서버에서 응답이 내려오면 데이터를 메모리 상에서 처리합니다.
.default, ehemeral, shared 세션에서는 지원하지만 .background 세션에서는 지원하지 않습니다.

URLSessionUploadTask

서버의 응답을 받아오기 전에 데이터를 업로드할 수 있도록 request body를 쉽게 제공한다는 점만 제외하면 dataTask와 동일합니다.
dataTask와는 다르게 .background 세션을 지원합니다.

URLSessionDownloadTask

리소스를 디스크의 파일로 직접 다운로드합니다.
UploadTask와 동일하게 모든 세션에서 지원합니다.
Task는 suspend 상태로 생성되기 때문에 resume()를 호출하여 task를 시작해야 합니다.
지금까지 URLSession과 그 안을 구성하는 URLSessionConfiguration 그리고 URLSession이 생성한 Task 까지 알아보았습니다.
네트워크를 구성하는게 어떤 건지는 알았는데, 그래서 네트워킹이 어떻게 진행되는 건지는 아직 감이 안잡히셨을겁니다. 네트워킹이 어떻게 진행되는지는 예시를 보면서 같이 알아봅시다.
총 두 가지 예시를 만들어보려고 합니다.
하나는 기본적인 데이터 수신 코드고, 다른 하나는 앱에서 서버로 데이터를 보내는 코드를 만들겁니다. 예시들은 Apple 공식 문서에 있는 Fetching Website Data into MemoryUploading Data to a Website 를 기반으로 했습니다.

 데이터 가져오기

dataTask 메소드를 사용해서 데이터를 수신하는 예제를 만들어보려고 합니다. 예제에서는 Unsplash에서 제공해주는 API를 사용합니다.
단순하게 사진만 가져오는 GET /photos API를 사용해서 화면에 사진을 띄워줄겁니다.
그 전에 사진을 띄울 화면을 만들어야겠죠? 저는 사진을 보여줄 UICollectionView를 만들고 CollectionView Cell에서 사진을 보여주도록 스토리보드를 구성했습니다.
이번 예제에서 중요한 건 네트워크 코드니깐 화면 구성에 대한 설명은 넘어가겠습니다. 전체적인 코드가 궁금하신 분들을 위해서 하단에 깃허브 링크 첨부해두겠습니다.
먼저, 데이터를 가져오기 위해서는 URLSession 인스턴스를 생성해야 합니다. 사진을 가져오는 건 복잡한 Configuration 사용자 커스텀 없이 필요없고, delegate callback과 상호작용할 필요도 크게 없으니, shared 인스턴스를 사용해서 구현할 수 있을 거 같네요.
그럼 dataTask 메소드를 사용해서 task를 생성합시다.
private func startLoad() { let url = URL(string: "https://api.unsplash.com/photos")! let task = URLSession.shared.dataTask(with: url) }
Swift
복사
dataTask 메소드에는 URL 또는 URLRequest 를 인수로 넣을 수 있습니다. URL 만 사용해도 원하는 사진 데이터를 GET 할 수 있기 때문에 굳이 URLRequest를 만들진 않았습니다.
우리는 해당 url에 데이터를 요청했습니다. 그렇다면 요청에 대한 응답 메시지는 어떻게 받을 수 있을까요?
바로, dataTask의 Completion Handler 를 통해서 받을 수 있습니다. 핸들러는 서버가 보낸 요청 받기를 완료했을 때 호출됩니다.
Completion Handler 에는 총 세 가지 값이 들어옵니다. data, response, error 입니다.
data : 서버에서 보내준 데이터
response : status code, MIME Type 같은 응답 메시지 관련 데이터
error : 에러가 발생했을 때 들어오는 데이터
위의 코드에서 Completion Handler 를 구현해봅시다.
private func startLoad() { let url = URL(string: "https://api.unsplash.com/photos")! let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { self.handleClientError(error) return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { self.handleServerError(response) return } if let mimeType = httpResponse.mimeType, mimeType == "application/json", let data = data { self.handleImages(data) } } }
Swift
복사
if let error = error
guard let httpResponse = response as? HTTPURLResponse
if let mimeType = httpResponse.mimeType
Task까지 다 만들었다면 이제 Task를 실행시켜줍시다. Task는 suspend 상태로 생성되기 때문에 resume() 를 호출하여 시작해야 합니다.
private func startLoad() { let url = URL(string: "https://api.unsplash.com/photos")! let task = URLSession.shared.dataTask(with: url) { data, response, error in // 생략 } task.resume() }
Swift
복사
그럼 어떻게 되는지 볼까요?
200대가 아니면 에러 알럿이 뜬다는 건 잘 알겠네요. ( ᐛ )و
401 상태 코드가 들어왔다는건 인증에 문제가 있다는 겁니다. 401은 클라이언트가 해당 리소스에 대한 인증이 필요하다는 걸 나타내기 때문이죠.(더 자세한 내용은 [Network] HTTP 상태코드 포스팅을 확인해주세요.)
헤더에 인증 관련 필드값을 넣어서 보냈어야 했는데 해당 부분이 누락된 거 같습니다.
Unsplash Documentation를 살펴보니깐, 이런 부분이 있더라구요.
아무래도 헤더 필드(Authorization) 값을 안보내서 생긴 문제같네요. 그러면 현재 예시에 인증을 위한 헤더 필드도 넣고 쿼리 파라미터도 넣는 작업을 해봅시다.
*인증이 필요없는 다른 url를 사용했다면 데이터가 제대로 올겁니다.

 헤더 필드, 쿼리 파라미터를 사용해서 데이터 가져오기

Authorization 헤더 필드의 부재로 401 인증 에러가 발생했습니다. 따라서, 우리는 Authorization 헤더 필드를 만들어서 인증 문제를 해결하려고 합니다.
그럼, 헤더 필드 정보는 어떻게 넣어주는가?
헤더 필드 정보를 넣기 위해서는 URLRequest 를 사용해야 합니다. URLRequest 는 로드할 URL과 사용되는 Policy 속성을 캡슐화하고 있는 개체입니다. 즉, 요청에 대한 정보를 나타내는 부분입니다.
만약 HTTP, HTTPS Scheme에 대해 요청할 경우, URLRequest 에는 HTTP 메서드, HTTP 헤더를 포함할 수 있게 됩니다. 우리는 현재 HTTPS Scheme에 대해서 요청 메시지를 보내는 것이기 때문에 HTTP 헤더를 포함할 수 있습니다. 그러면 HTTP 헤더 필드를 넣어볼까요?
일단, 헤더 필드를 구성하기 전에 URLRequest 인스턴스부터 만들어 보겠습니다.
let url = URL(string: "https://api.unsplash.com/photos")! var urlRequest = URLRequest(url: url)
Swift
복사
URLRequest init 메서드에는 url, cachePolicy, timeInterval 값을 넣을 수 있는데, url 말고 나머지 두 인수는 크게 조정이 필요하지 않기 때문에 url만 넣어주겠습니다.
그리고 URLRequest에서 HTTP 헤더 값을 설정해봅시다. URLRequest 내부에는 allHTTPHeaderFields 라는 속성이 있습니다. 해당 속성에 key-value 형식으로 헤더 필드와 값을 넣어주면 됩니다.
우리는 Authorization 헤더 필드를 설정해주어야 합니다.
urlRequest.allHTTPHeaderFields = ["Authorization": "Client-ID \(clientId)"]
Swift
복사
Authorization 헤더 필드를 설정했으니, 해당 URLRequest 인스턴스를 dataTask 메소드에 넣어줍니다. 이전에 url를 넣었던 부분에 URLRequest 를 넣어주면 됩니다.
dataTask는 커스텀한 요청 메시지가 없다면 기본적인 URL 만 받고, 요청 메시지에 사용자가 추가적으로 내용을 넣었다면 URLRequest 를 인수로 받습니다. 그렇기 때문에, URL 부분에 URLRequest 를 넣어줘도 큰 문제가 발생하지 않을겁니다.
private func startLoad() { let url = URL(string: "https://api.unsplash.com/photos")! var urlRequest = URLRequest(url: url) urlRequest.allHTTPHeaderFields = ["Authorization": "Client-ID \(clientId)"] let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in // ... } task.resume() }
Swift
복사
이제 헤더 필드를 넣었으니 다시 프로젝트를 빌드해볼까요?
인증 문제를 해결했더니 이미지가 잘 들어옵니다. ٩( ᐛ )
이번엔 쿼리 파라미터를 한 번 넣어봅시다.
GET /photos 에는 3가지 쿼리 파라미터가 있습니다. 그 중에서 per_page, order_by 값을 쿼리 파라미터로 보내보려고 합니다.
쿼리 파라미터는 어떻게 넣는가?
URLQueryItem 를 사용하면 됩니다. URLQueryItem 은 name, value 부분에 query item과 query value를 넣을 수 있습니다. 생성된 URLQueryItem 은 URL에 있는 append(queryItems:) 메서드를 사용해서 넣어줄 수 있습니다. 저는 따로 URLQueryItem 인스턴스를 생성하지 않고 append 메서드에서 바로 추가해줄게요.
var url = URL(string: "https://api.unsplash.com/photos")! url.append(queryItems: [ URLQueryItem(name: "per_page", value: "20"), URLQueryItem(name: "order_by", value: "popular") ])
Swift
복사
페이지 내부 사진 갯수, 정렬 순서를 변경할 수 있는 UI가 따로 없으니 value를 하드코딩해서 넣어주겠습니다.
쿼리 파라미터를 넣고 빌드해보면?
전과는 조금 다른 사진들이 20개 들어오는걸 확인할 수 있어요! (ง ᵕᴗᵕ)ว

 데이터 보내기

위에서는 GET 메서드를 사용해서 데이터를 가져오기만 했으니, request body를 사용해서 데이터를 보내보는 예시도 한 번 만들어볼게요.
POST /collections API를 사용해서 사진들을 저장할 수 있는 Collection를 만들어 보겠습니다.
일단, 다른 스토리보드를 하나 만들어서 Collection 생성에 필요한 파라미터 값을 설정할 수 있는 화면을 만들겠습니다. title, description 를 작성할 수 있는 TextField를 하나 만들고, private를 설정할 수 있는 버튼을 하나 만들어야겠군요.
Collection 생성 화면을 만들었습니다. 이제 POST 요청을 보내는 코드를 만들어 볼까요.
먼저, URLSession 인스턴스를 생성해서 URLSessionUploadTask를 생성합니다. 데이터 업로드 시에는 서버 응답을 받아오기 전에 요청 부분에서 데이터를 업로드해야하기 때문에 uploadTask 를 사용할 겁니다.
let task = URLSession.shared.uploadTask(with: URLRequest, from: Data)
Swift
복사
uploadTask 를 사용하려고 봤더니 두 가지 파라미터가 필요하네요. 요청 메시지 정보를 가지고 있는 URLRequest와 요청 바디 Data 입니다. 그럼 두 부분을 먼저 설정해볼게요.
URLRequest
Data
uploadTask 메서드도 completion handler를 사용해서 응답 값을 다룹니다. 위에서 각 부분에 대해서 설명했기 때문에 이번에는 코드만 보고 지나가겠습니다.
private func uploadCollection(_ collection: CollectionRequestDTO) { guard let data = self.encodeCollection(collection) else { return } let task = URLSession.shared.uploadTask(with: self.urlRequest, from: data) { data, response, error in if let error = error { self.handleClientError(error) return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { self.handleServerError(response) return } if let mimeType = httpResponse.mimeType, mimeType == "application/json", let data = data { self.handleCollection(data) } } }
Swift
복사
handleCollection 에서는 응답 데이터가 제대로 들어오면 화면이 dismiss 되도록 코드를 구현했습니다.
Task까지 다 만들었다면 이제 Task를 실행시켜줍시다. Task는 suspend 상태로 생성되기 때문에 resume() 를 호출하여 시작해야 합니다.
private func uploadCollection(_ collection: CollectionRequestDTO) { guard let data = self.encodeCollection(collection) else { return } let task = URLSession.shared.uploadTask(with: self.urlRequest, from: data) { data, response, error in // 생략 ... } task.resume() }
Swift
복사
이제 Collection를 한 번 생성해볼까요?
장렬하게 400를 만났습니다. (〃⌒∇⌒) …
에러를 확인해보니깐, title is missing 이라고 하네요. 그럴리가 없습니다. 저는 title 를 잘 보냈는걸요.
Postman에서는 Collection이 잘 만들어져서 1시간동안 디버깅을 하다가 혹시 이 헤더 필드를 안넣어서 생긴걸까 했는데, 그런거였습니다. ^^
urlRequest.allHTTPHeaderFields = [ "Authorization": "Bearer \(AccessToken)", "Content-Type": "application/json" ]
Swift
복사
"Content-Type": "application/json" 넣어주셔야 합니다. 설정해주지 않는다면 Content-Type 헤더 필드에 appliction/json 말고 다른 형식이 들어가는 거 같습니다. 그래서 JSON 형식으로 넣었던 데이터를 확인하지 못한거구요.
다시 한 번 Collection를 생성해보겠습니다.
생성 후에 화면이 잘 내려가는걸 보아 생성이 잘 되었나 봅니다.
Unsplash 사이트에서 제 Collection를 확인해보면 카테고리가 잘 생성되었군요.

 마치며

이번 포스팅에서는 Shared Session를 사용해서 데이터를 가져오고 보내보는 코드를 짜봤습니다.
이어지는 포스팅에서는 URLSessionConfiguration, Delegate를 사용해서 동일한 일을 하는 코드를 짜보겠습니다. 그리고 현재 코드를 공통화하여 async-await를 사용해 코드를 짜보려고 합니다.
그럼 다음 포스팅에서 뵈어요! ๑・̑◡・̑๑
포스팅에서 사용한 코드 예시는 링크를 통해서 보실 수 있습니다.

 참고자료