iOS) Alamofire 해체쇼

4 minute read

따란, URLSession 을 공부하면서 Alamofire 에서는 어떻게 구현했을까? 라는 궁금증에서 시작된 라이브러리 해체쇼

내용

  • upload task 와 data task 으로 POST 통신을 구현할 수 있다. Alamofire 에서는 어떻게 구현하고 있을까?
  • Multipart/form-data POST 통신을 Alamofire 에서는 어떻게 구현하고 있을까?

upload task 와 data task 으로 POST 통신을 구현할 수 있다. Alamofire 에서는 어떻게 구현하고 있을까?

URLSessionDataTask 로 URLRequest 에 httpBody 를 설정해서 POST 통신을 할 수 있다.

또한, POST 통신을 목적으로 한 URLSessionUploadTask 를 사용할 수도 있다.

  • URLSessionDataTask : Data task 는 리소스를 요청하고 응답받아 NSData 객체로 반환. default, ephemeral, shared session 에서는 지원되지만 background sessions 에서는 지원되지 않는다.
  • URLSessionUploadTask : Upload task 는 데이터를 업로드할 수 있도록 request body 를 제공한다. background session 도 지원된다.

Alamofire 는 어떤 task 를 사용하는지 해체쇼를 해봤다. URLSessionUploadTask 를 사용하고 있었다.

// 📌 Request.swift
public class UploadRequest: DataRequest {
    //...
    }

    // ...

    override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
        // ...
        switch uploadable {
        case let .data(data): return session.uploadTask(with: request, from: data)
        case let .file(url, _): return session.uploadTask(with: request, fromFile: url)
        case .stream: return session.uploadTask(withStreamedRequest: request)
        }

    // ...
}

Multipart/form-data POST 통신을 Alamofire 에서는 어떻게 구현하고 있을까?

  • 먼저, URLSession 을 활용해서 직접 구현해보자.
// POST - Image 송신
    // ✅ data : UIImage 를 pngData() 혹은 jpegData() 사용해서 Data 로 변환한 것.
    // ✅ filename : 파일이름(img.jpg 과 같은 이름)
    // ✅ mimeType :  타입에 맞게 png면 image/png, text text/plain 등 타입.
    func requestPOSTWithMultipartform(url: String,
                                      parameters: [String : String],
                                      data: Data,
                                      filename: String,
                                      mimeType: String,
                                      completionHandler: @escaping (Bool, Any) -> Void) {
        
        guard let url = URL(string: url) else {
            print("Error: cannot create URL")
            return
        }
        
        // ✅ boundary 설정
        let boundary = "Boundary-\(UUID().uuidString)"
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        // request.httpBody = uploadData
        
        // ✅ data
        var uploadData = Data()
        let imgDataKey = "img"
        let boundaryPrefix = "--\(boundary)\r\n"
        
        for (key, value) in parameters {
            uploadData.append(boundaryPrefix.data(using: .utf8)!)
            uploadData.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
            uploadData.append("\(value)\r\n".data(using: .utf8)!)
        }
        
        uploadData.append(boundaryPrefix.data(using: .utf8)!)
        uploadData.append("Content-Disposition: form-data; name=\"\(imgDataKey)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        uploadData.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
        uploadData.append(data)
        uploadData.append("\r\n".data(using: .utf8)!)
        uploadData.append("--\(boundary)--".data(using: .utf8)!)

        
        let defaultSession = URLSession(configuration: .default)
        // ✅ uploadTask(with:from:) 메서드 사용해서 reqeust body 에 data 추가.
        defaultSession.uploadTask(with: request, from: uploadData) { (data: Data?, response: URLResponse?, error: Error?) in
            // ...
        }.resume()
    }

그렇다면 Alamofire 는 위와 같은 폼을 어떻게 구현하고 있을까?

먼저 간단하게 Alamofire 로 구현해보자

AF.upload(multipartFormData: { multipartFormData in
    multipartFormData.append(images, withName: "img", fileName: "image1", mimeType: "image/jpeg")

    // ...
}
  • upload() 메서드는 다음과 같이 구현되어 있다.
// 📌 Session.swift
open func upload(multipartFormData: @escaping (MultipartFormData) -> Void,
                     to url: URLConvertible,
                     usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold,
                     method: HTTPMethod = .post,
                     headers: HTTPHeaders? = nil,
                     interceptor: RequestInterceptor? = nil,
                     fileManager: FileManager = .default,
                     requestModifier: RequestModifier? = nil) -> UploadRequest {
// ...
}

위의 multipartFormData 파라미터의 append() 메서드로 parameter 들을 추가하고 있다. 알아보자.

// 📌 MultipartFormData.swift

// 1️⃣
    /// Creates a body part from the data and appends it to the instance.
    ///
    /// The body part data will be encoded using the following format:
    ///
    /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
    /// - `Content-Type: #{mimeType}` (HTTP Header)
    /// - Encoded file data
    /// - Multipart form boundary
    ///
    /// - Parameters:
    ///   - data:     `Data` to encoding into the instance.
    ///   - name:     Name to associate with the `Data` in the `Content-Disposition` HTTP header.
    ///   - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header.
    ///   - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header.
    public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) {
        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
        let stream = InputStream(data: data)
        let length = UInt64(data.count)

        append(stream, withLength: length, headers: headers)
    }

// ...

// 2️⃣
// MARK: - Private - Content Headers

    private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders {
        var disposition = "form-data; name=\"\(name)\""
        if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" }

        var headers: HTTPHeaders = [.contentDisposition(disposition)]
        if let mimeType = mimeType { headers.add(.contentType(mimeType)) }

        return headers
    }

// 3️⃣
// ✅ 1️⃣ 에서 마지막 append(_:withLength:headers:) 함수.
    /// - Parameters:
    ///   - stream:  `InputStream` to encode into the instance.
    ///   - length:  Length, in bytes, of the stream.
    ///   - headers: `HTTPHeaders` for the body part.
    public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
        let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
        bodyParts.append(bodyPart)
    }

// 4️⃣
class BodyPart {
        let headers: HTTPHeaders
        let bodyStream: InputStream
        let bodyContentLength: UInt64
        var hasInitialBoundary = false
        var hasFinalBoundary = false

        init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
            self.headers = headers
            self.bodyStream = bodyStream
            self.bodyContentLength = bodyContentLength
        }
    }

// ... 

위 부분을 보면 얼추 비슷하게 꾸리고 있다는게 보여진다. 조금 더 해체해 보자.. 슥삭🔪.. 가만히 있거라 요놈아..

// 📌 HTTPHeaders.swift
// 2️⃣ 와 연결된다.
public static func contentDisposition(_ value: String) -> HTTPHeader {
        HTTPHeader(name: "Content-Disposition", value: value)
    }

// ... 

public static func contentType(_ value: String) -> HTTPHeader {
        HTTPHeader(name: "Content-Type", value: value)
    }

그리고 아래의 boundaryDaya() 메서드를 보게되면 폼을 만들수 있는 로직이 있다.

// 📌 MultipartFormData.swift

open class MultipartFormData {
    // MARK: - Helper Types

    enum EncodingCharacters {
        static let crlf = "\r\n"
    }

    enum BoundaryGenerator {
        enum BoundaryType {
            case initial, encapsulated, final
        }

        static func randomBoundary() -> String {
            let first = UInt32.random(in: UInt32.min...UInt32.max)
            let second = UInt32.random(in: UInt32.min...UInt32.max)

            return String(format: "alamofire.boundary.%08x%08x", first, second)
        }

        static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
            let boundaryText: String

            switch boundaryType {
            case .initial:
                boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
            case .encapsulated:
                boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
            case .final:
                boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
            }

            return Data(boundaryText.utf8)
        }
    }

Categories:

Updated: