안드로이드 Bitmap 최적화(Resize)한 다중 이미지 서버에 업로드하기 2 - 서버로 보내기 (코루틴(Coroutine), Retrofit2)

임현주·2022년 8월 31일
2
post-thumbnail

시작

혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 잘못 이해하고 작성한 부분에 대한 피드백을 주신다면 감사히 받겠습니다 🙇🏻‍♀️


이제 우리는 지난 시간 열심히 다이어트 시켜준 비트맵을 서버로 떠나 보낼 일만 남았다. 이 과정은 무던히 잘 넘길 줄 알았으나.. 비트맵을 압축하는 과정에서 파일 용량에 따라 속도가 다르다보니 기존(사용자가 선택한 순서)과 다르게 순서가 꼬이는 사태가 발생했다.

순서를 지키기 위해 사진 한 장이 압축될 때까지 기다렸다가 다음 사진을 압축하자니, 운 나쁘게 대용량 사진 10장을 업로그 해야 한다면?! 10초 이상의 시간이 지체될 것이고 사용자는 이를 무한로딩으로 인식하고 앱을 종료해버리고 말 것이다 🥲

🎈 서버로 보낼 여러 개의 이미지(file) 담기

위의 문제를 해결하기 위해 HashMap에 저장한 후, 사용자가 선택한 사진 순서에 맞게 arrayList에 다시 저장하여 반환하는 방법을 생각했다.

❓ List가 아닌 HashMap에 저장하는 이유는 위에 언급한 것처럼 압축이 완료되는 순서가 달라서 IndexOutOfBoundsException이 발생 할 수 있기 때문이다.

  1. HashMap을 이용해 indexpath를 저장한다.
    Key ➡️ index (사용자가 선택한 사진의 순서)
    Value ➡️ 최적화된 bitmap의 임시 저장 경로 (지난 시간에 FileUtil에서 return 받은 값)

  2. 이 때, 코루틴 빌더의 join() 메소드를 통해 압축 작업이 전부 완료될 때까지 기다려준다.

  3. arrayList에 HashMap의 Value 값을 FormData 에 담아 순서대로 저장하여 반환한다.
object FileUtil {
    
    ...
    
    suspend fun bitmapResize(
        context: Context,
        uriList: ArrayList<Uri>
    ): MutableList<MultipartBody.Part>? {

        val pathHashMap = hashMapOf<Int, String?>()

        CoroutineScope(Dispatchers.IO).launch {
            uriList.forEachIndexed { index, uri ->
                launch {
                	// 지난 시간에  FileUtil에서 return 받은 값
                    val path = optimizeBitmap(context, uri)
                    pathHashMap[index] = path
                }
            }
        }.join() // 작업이 끝날 때까지 기다린다.

        val fileList = arrayListOf<MultipartBody.Part>()
        
        pathHashMap.forEach {
            if (it.value.isNullOrEmpty()) {
                return null
            }

            val filePart = addImageFileToRequestBody(it.value!!, "files")
            fileList.add(filePart)
        }

        return fileList
    }

    // 이미지 'FormData' 에 담기
    fun addImageFileToRequestBody(path: String, name: String): MultipartBody.Part {
        val imageFile = File(path)
        
        // MIME 타입을 따르기 위해 image/jpeg로 변환하여 RequestBody 객체 생성
        val fileRequestBody = imageFile.asRequestBody("image/jpeg".toMediaTypeOrNull())
        // RequestBody로 Multipart.Part 객체 생성
        return MultipartBody.Part.createFormData(name, imageFile.name, fileRequestBody)
    }
    
    ...
  
}

Retrofit2을 통해 서버로 파일을 전송할 때는 Multipart를 이용해야한다.

Multipart란?

HTTP를 통해 File을 Server로 전송하기 위해 사용되는 Content-type 이다.

HTTP 프로토콜은 크게 HeaderBody 로 구분 되며 Body 에 데이터를 담아 전송한다.
이 때, Body 에 들어가는 데이터 타입을 명시해주는 것이 Content-type 이다.

Content-type 필드에 MIME(Multipurpose Internet Mail Extensions) 타입을 지정해줄 수 있다. Multipart(=multipart/form-data)는 MIME 타입 중 하나이다.

addImageFileToRequestBody() 메소드에서 createFormData()를 통해 form-data 에 file을 담아 객체를 생성하여 반환해준다.

createFormData(name: String, filename: String?, body: RequestBody)
  • name : 서버와 약속한 key 값
  • filename : 파일 이름
  • body : RequestBody 객체

🎈 Retrofit2로 파일 업로드

object RetrofitClient {

    private val gson = GsonBuilder().setLenient().create()

    private val clientBuilder = OkHttpClient.Builder()
    private val loggingInterceptor = HttpLoggingInterceptor()

    init {
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY;
        clientBuilder.addInterceptor(loggingInterceptor)
    }

    val retrofitClient: Retrofit = Retrofit.Builder()
        .baseUrl(API.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(clientBuilder.build())
        .build()
        
    val fileApiService: FileService = retrofitClient.create(FileService::class.java)
    
}
interface FileService {

    // 다중 파일 업로드
    @Multipart
    @POST(API.FILE_UPLOAD)
    fun uploadFileList(
        @Part files: List<MultipartBody.Part>
    ): Response<Long>
    
}

파일 업로드 관련 Retrofit Service를 작성할 때는 반!드!시! @Multipart 어노테이션을 작성해야하는 것을 잊지말자 !!

(필자는 원래 RetrofitClient를 작성할 때, Koinsingle{} 을 사용해 싱글톤 객체를 생성하여 의존성 주입(DI)을 하지만 포스팅하기 위해 수정하여 올린다.)

private suspend fun uploadPhoto() = withContext(Dispatchers.IO) {

    val fileList = FileUtil.bitmapResize(context!!, uriList)

    if (fileList.isNullOrEmpty()) {
    	// bitmapResize 실패시
        return
    }
    
    val response = fileApiService.uploadFileList(fileList)
    
    if (response.isSuccessful) {
    	// 파일 업로드 성공
    } else {
    	// 파일 업로드 실패
    }
}

최종적으로 필요한 곳에 uploadPhoto() 메소드를 호출해주면 끝이다 !


실제로 필자는 이번 과정을 통해 이미지 업로드만 10초 ~ 15초 정도 걸리던 과정을 약 2초 대로 줄일 수 있었다 🥹 (원본 이미지 크기와 서버와 통신 과정에 따라 최종적인 업로드 시간에 개인차가 있을 수 있겠지만 👉🏻👈🏻 )

이전에 작성한 Bitmap Resize 부분은 몇 달 전에 작성한 코드라 미숙한 부분이 많지만 시간이 된다면 보다 깔끔한 코드로 업데이트 해보도록 노력해야겠다 !

오늘도 읽어주셔서 감사합니다 🙇🏻‍♀️ !!

profile
🐰 피드백은 언제나 환영합니다

0개의 댓글