URLSession으로 multipart/form 방식으로 데이터 전송

URLSession으로 http프로토콜을 이용해 POST, PUT 통신을 할 때 기본적으로 Reqeust의 헤더에 Content-Type에 application/json을 붙여서 사용했는데, 이번에는 http프로토콜을 이용해 multipart/form으로 이미지를 전송해 보았다.

 

multipart/form 방식은 기존 application/json과 다르게 body를 json으로 보내는 것이 아닌 보내는 데이터를 여러 개로 쪼개서 보내는 방식이다.

 

 

Multipart/form Header

func settingHeader(identifier: String, request: URLRequest) -> URLRequest {
        var copyReq = request
        let contentType = "multipart/form-data; boundary=\(identifier)"
        copyReq.addValue(contentType, forHTTPHeaderField: "Content-Type")
        return copyReq
    }

header의 boundary에 들어가는 값은 multipart의 나뉜 부분을 구분하기위해 사용되는 값이다. 이 boundary 값으로 데이터가 나뉜 부분과 어디서부터 시작이고 어디서부터 끝인지를 구분한다.

Multipart/form Body

만약 요청 body 파라미터가 다음과 같다고 해보자.

Key value type 설명
name String  
food String  
petImage Data 최대 2장

petImage는 jpeg 이미지를 보낸다고 가정했을때, request의 body는 다음과 같은 형식으로 이루어져 있다.

Content-Type: multipart/form-data; boundary=50178FC4-8E6A-4028-A194-E0B460FA4D4B

--50178FC4-8E6A-4028-A194-E0B460FA4D4B
Content-Disposition: form-data; name="name"

이름
--50178FC4-8E6A-4028-A194-E0B460FA4D4B
Content-Disposition: form-data; name="food"

음식
--50178FC4-8E6A-4028-A194-E0B460FA4D4B
Content-Disposition: form-data; name="petImage"; filename="profile50178FC4-8E6A-4028-A194-E0B460FA4D4B.jpg"
Content-Type: image/jpeg

<이미지 데이터>
--50178FC4-8E6A-4028-A194-E0B460FA4D4B
Content-Disposition: form-data; name="petImage"; filename="profile50178FC4-8E6A-4028-A194-E0B460FA4D4B.jpg"
Content-Type: image/jpeg

<이미지 데이터>
--50178FC4-8E6A-4028-A194-E0B460FA4D4B--

 

 

기존 json과 다르게 파라미터 값이 모두 구분되어져 있어야 하고, 띄어쓰기 역시 정확히 되어 있어야 한다. 만약 데이터가 들어가는 영역에 엔터가 한 줄 더 들어가거나 덜 들어가는 경우 데이터가 제대로 인식이 안되거나 포맷이 하나의 데이터로 인식되므로 주의할 것.

코드

Encoding할 값과 전달할 데이터의 값을 따로 구분. 

Encoding 할 객체를 JSONSerialization을 이용하여 Dictionary 형태로 변환, 전달할 데이터 역시 Dictionary 형태로 값 전달

let identifier = UUID().uuidString
let body = try JSONEncoder().encode(bodyParameter)
let bodyParameterDict = try JSONSerialization.jsonObject(with: body) as? [String: Any]
let strBodyDict = bodyParameterDict.mapValues { String(describing: $0) }
request.httpBody = uploader.createPostBody(identifier: identifier, bodyParameterData: strBodyDict, datas: data)

Encoding 한 바디 파라미터 데이터를 Data 객체에 추가. 추가할 때마다 boundary값을 넣어주고, name에 파라미터의 이름값을 삽입. 

func createPostBody(identifier: String, bodyParameterData: [String : String], datas: [String : [Data]]) -> Data {
        let convertData = NSMutableData()
        for parameter in bodyParameterData {
            convertData.appendString("\r\n--\(identifier)\r\n")
            convertData.appendString("Content-Disposition: form-data; name=\"\(parameter.key)\"\r\n\r\n")
            convertData.appendString(parameter.value)
        }
        
        convertData.append(convertDatas(identifier: identifier, datas: datas))
        convertData.appendString("--\(identifier)--\r\n")
        return convertData as Data
    }
    
 extension NSMutableData {
    func appendString(_ string: String) {
        if let data = string.data(using: .utf8) {
            self.append(data)
        }
    }
}

전달할 데이터는 Content-Type을 지정하여 어떤 데이터인지 표시후 데이터 삽입. 나의 경우 jpeg데이터를 전달하기에 jpeg로 따로 표기해 두었지만, 함수로 인자값을 받는 방식으로 구현해도 될 듯하다.

func convertDatas(identifier: String, datas: [String : [Data]]) -> Data {
        let convertData = NSMutableData()
        for data in datas {
            for detailData in data.value {
                convertData.appendString("\r\n--\(identifier)\r\n")
                convertData.appendString("Content-Disposition: form-data; name=\"\(data.key)\"; filename=\"\(data.key)\(identifier).jpg\"\r\n")
                convertData.appendString("Content-Type: image/jpeg\r\n\r\n")
                convertData.append(detailData)
                convertData.appendString("\r\n")
            }
        }
        
        return convertData as Data
    }

 

전체적인 코드: https://github.com/Kim-Junhwan/LSLP/tree/profile/LowServiceLevelProject/Network

 

참고 : https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/POST