혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 잘못 이해하고 작성한 부분에 대한 피드백을 주신다면 감사히 받겠습니다 🙇🏻♀️
이제 우리는 지난 시간 열심히 다이어트 시켜준 비트맵을 서버로 떠나 보낼 일만 남았다. 이 과정은 무던히 잘 넘길 줄 알았으나.. 비트맵을 압축하는 과정에서 파일 용량에 따라 속도가 다르다보니 기존(사용자가 선택한 순서)과 다르게 순서가 꼬이는 사태가 발생했다.
순서를 지키기 위해 사진 한 장이 압축될 때까지 기다렸다가 다음 사진을 압축하자니, 운 나쁘게 대용량 사진 10장을 업로그 해야 한다면?! 10초 이상의 시간이 지체될 것이고 사용자는 이를 무한로딩으로 인식하고 앱을 종료해버리고 말 것이다 🥲
위의 문제를 해결하기 위해 HashMap에 저장한 후, 사용자가 선택한 사진 순서에 맞게 arrayList에 다시 저장하여 반환하는 방법을 생각했다.
❓ List가 아닌 HashMap에 저장하는 이유는 위에 언급한 것처럼 압축이 완료되는 순서가 달라서 IndexOutOfBoundsException이 발생 할 수 있기 때문이다.
- HashMap을 이용해
index
와path
를 저장한다.
Key
➡️index
(사용자가 선택한 사진의 순서)
Value
➡️최적화된 bitmap의 임시 저장 경로
(지난 시간에 FileUtil에서 return 받은 값)
- 이 때, 코루틴 빌더의
join()
메소드를 통해 압축 작업이 전부 완료될 때까지 기다려준다.
- 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 프로토콜은 크게Header
와Body
로 구분 되며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)
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를 작성할 때, Koin의 single{}
을 사용해 싱글톤 객체를 생성하여 의존성 주입(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 부분은 몇 달 전에 작성한 코드라 미숙한 부분이 많지만 시간이 된다면 보다 깔끔한 코드로 업데이트 해보도록 노력해야겠다 !
오늘도 읽어주셔서 감사합니다 🙇🏻♀️ !!