Retrofit에서 Multipart 서버 통신 with Kotlin

룰루랄라~ 난 이제 GET 요청 POST 요청 다 잘 보내는 API 장인이다!
라고 생각이 들 때, 한번쯤 사진을 전송하는 멀티파트 서버 통신에도 도전해 보시길 바란다.

멀티파트 서버 통신 시작하기

보통 이미지를 보내는 경우는 무언가 작성할 때, 즉 생성할 때와 관련이 있으므로 POST 요청을 보내야 한다.
POST 요청을 할 때는 body에 값을 담아 보냈었는데,
이미지 / 문서 등 사이즈가 큰 데이터를 서버로 전송할 때는 다른 통신 방식이 필요하다.

멀티파트는 다양한 타입의 데이터를 전송할 때 사용하는 데이터 타입이다.
인스타그램에서 글을 업로드할 때를 생각해 보면,
+ 사진 + 동영상 + 함께한 유저를 한 번에 묶어서 서버로 보내야 한다.

이렇게 여러 종류의 데이터를 구별해서 Body에 넣어주기 위한 방법이 Multipart 타입이다.
Type-safe HTTP client for Android and Java인 Retrofit의 문서를 읽으며 어떻게 구현할지 방법을 생각해보자!

Retrofit 공식 문서 읽어보기


Retrofit 공식 문서

원래 POST 요청을 보내기 위해서는 @BODY를 달아서 한 방에 넣어줬지만, 멀티파트 통신에 쓰이는 모든 요소들은 이미지, 숫자, 문자 모두 @PART를 달아서 넣어줘야 하며,
이 때 자료형은 모두 Retrofit에서 제공해주는 RequestBody 클래스를 상속받은 자료형이여야 한다.

@MultiPart
@POST("user/photo")
fun updateUser(
	@Part photo: RequestBody, 
	@Part description: RequestBody
) : Call<User>

좀 서운하게 자바 코드만 써줬길래 ,, 코틀린 코드로 번역해봤다.
인터페이스 속 함수는 이렇게 정의하고, 실제로 API를 쏘는 곳에서는 보낼 데이터를 모두 RequestBody로 변환해주어야 한다.

Service 인터페이스 만들어보기 (with Kotlin)


이렇게 데이터를 보내야 하는 상황을 가정해보자.
여기서 imageform-data/file,
contentyear/month/dayString,
indexInt 형이다.

하지만 뚝심 있게 모두 RequestBody로 넣어줬다.
나의 경우 image를 Bitmap 형태로 줘야 했기에 MultipartBody.Part라는 자료형을 정의하여 사용했다.
image를 제외한 자료들은 모두 data라는 해쉬맵에 넣어 줬다.

@Multipart
@POST("activity/new")
fun addActivity(
    @Part image: MultipartBody.Part?,
    @PartMap data: HashMap<String, RequestBody>
): Call<ResponseWrapperEmptyData>

API를 불러 보기

API를 호출할 때는 아래와 같이 호출한다.
HashMap에 담을 때는 RequestBody로 바꿔주고 key값은 API 명세서에 적힌 대로 적어주면 된다.
이미지 비트맵은 BitmapRequestBody로 형변환해준 이후 MultipartBody로 바꾸어 준다. MultipartBody.Part.createFormData()에서 첫 인자로 API 명세서에 적힌 값을 주면 성공적으로 보낼 수 있다.

fun sendAddRequest() {
    viewModelScope.launch {
        val contentRequestBody : RequestBody = content.toPlainRequestBody()
        val yearRequestBody: RequestBody = year.toPlainRequestBody()
        val monthRequestBody: RequestBody = month.toPlainRequestBody()
        val dayRequestBody: RequestBody = date.toPlainRequestBody()
        val indexRequestBody: RequestBody = index.toString().toPlainRequestBody()
        val textHashMap = hashMapOf<String, RequestBody>()
        textHashMap["content"] = contentRequestBody
        textHashMap["year"] = yearRequestBody
        textHashMap["month"] = monthRequestBody
        textHashMap["day"] = dayRequestBody
        textHashMap["index"] = indexRequestBody
        
        val bitmapRequestBody = bitmap?.let { BitmapRequestBody(it) }
        val bitmapMultipartBody: MultipartBody.Part? =
            if (bitmapRequestBody == null) null 
            else MultipartBody.Part.createFormData("image", "seojin", bitmapRequestBody)

        val response = api.addActivity(bitmapMultipartBody, textHashMap).awaitResponse()
        ...
        
        }
    }

inner class BitmapRequestBody(private val bitmap: Bitmap) : RequestBody() {
    override fun contentType(): MediaType = "image/jpeg".toMediaType()
    override fun writeTo(sink: BufferedSink) {
        bitmap.compress(Bitmap.CompressFormat.JPEG, 99, sink.outputStream())
    }
}

private fun String?.toPlainRequestBody() = requireNotNull(this).toRequestBody("text/plain".toMediaTypeOrNull())

마지막 줄은 StringPlain Text RequestBody로 바꿔주는 확장함수인데, 저작권은 l2hyunwoo에게 있다.

참고 블로그

좋은 웹페이지 즐겨찾기