-
[Android] Retrofit2으로 서버에 Multipart로 이미지 전송하기Android📱 2022. 8. 13. 20:19
사이드 프로젝트 2개에 과제다 면접이다 정신이 없어서 포스팅을 게을리했네요 ㅠ
(코로나도 걸림 ㅠ)
이번에 프로젝트를 진행하면서 서버에 사진을 전송하는 작업을 할 기회가 생겨서
그 방법과 Multipart에 대해서 공유해보려 합니다.
우선 Multipart에 대해서 알 필요가 있을 거 같습니다.
Multipart란?
- HTTP 요청의 한 종류로 파일이나 데이터를 보내는 형식 중 하나입니다.
- 클라이언트에서 요청을 보낼 때, http 프로토콜 바디에 데이터를 나누어서 보내는 것입니다.
안드로이드 서버에 사진 보내기
우선 클라이언트에서 서버로 이미지를 업로드하려면 Multipart라는 것을 사용해야 한다는 것을 알았습니다.
그렇다면 어떻게 Android에서 Multipart로 사진을 전송할 수 있을까요?
전송은 Android를 좀 해보신 분들이라면 한 번쯤 들어보셨을 Retrofit2에서 간단하게 지원해줍니다.
그러니 우리는 이미지를 File로 만들고, 그것을 MultipartBody.Part로 변화하는 작업만 진행해주면 됩니다.
이 방법은 인터넷에 나와있는 게 많아서 저도 비슷할 겁니다 ㅎㅎ
나의 문제
그런데 저의 문제는 여기서 발생하였습니다.
현재 해결해야 하는 문제가 갤러리에서 가져온 사진을 File -> MultipartBody.Part로 만들어 줘야 하는데
이때 파일의 절대 경로가 필요했습니다.
하지만 Android의 보안 정책으로 절대 경로를 가져오는 것에 실패해서 그 이유를 찾아 서치의 여행을~~
하다가 앨범에 접근할 때 ACTION_GET_CONTENT가 아니라 ACTION_PICK으로 인텐트를 주고 MediaStore.Images.Media.EXTERNAL_CONTENT_URI를 옆에다 주면!!
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
이 형태로 접근을 하게 되면 Cursor를 이용해서 절대 경로를 가져올 수 있다는 것을 알게 되었습니다.
그런데 ACTION_PICK을 사용하면 안 된다는 글들을 또 보게 되어서 ACTION_GET_CONTENT를 써서 하는 방법을 찾아보다가 ㅡㅡ
그만 하루가 가버렸습니다....
그래서 일단 2가지의 방법을 찾았고, ACTION_PICK과 ACTION_CONTENT의 차이를 알아서 그냥 ACTION_PICK을 사용할까 고민이 중입니다.
두가지 방법은 나중에 포스팅 해보겠습니다!
- 갤러리 사진 가져오기
binding.cvUploadPic.setOnClickListener { val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) albumLauncher.launch(Intent.createChooser(intent, "Select Picture")) }
- registerForActivityResult 만들기 (사진을 골라 왔을 때 활동)
private val albumLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK && it.data != null) { try { val clipData = it?.data?.clipData val clipDataSize = clipData?.itemCount if (clipDataSize != null && clipDataSize > 6) { Toast.makeText(context, "사진은 6개까지 선택이 가능합니다.", Toast.LENGTH_SHORT).show() return@registerForActivityResult } logger("selectedImageUri : ${it?.data?.data}") if (clipData == null) { val selectedImageUri = it?.data?.data ?: Uri.EMPTY viewModel.setPictureUri(selectedImageUri) } else { clipData.let { data -> for (i in 0 until clipDataSize!!) { val selectedImageUri = data.getItemAt(i).uri ?: Uri.EMPTY viewModel.setPictureUri(selectedImageUri) } } } } catch (e: Exception) { e.printStackTrace() } } else if (it.resultCode == RESULT_CANCELED) { Toast.makeText(this.requireContext(), "사진 선택 취소", Toast.LENGTH_LONG).show() } else { logger("something wrong") } }
- 절대 경로 받아오기
- 저는 ViewModel에서 StatFlow를 통해서 사진의 Uri를 관리하고 변환했기 때문에 FileController라는 클래스를 만들어서 진행했습니다. 각자 방식에 맞춰서 진행하시면 되겠습니다.
fun uriToMultiPart(imageUri : Uri): MultipartBody.Part { val proj: Array<String> = arrayOf(MediaStore.Images.Media.DATA) val c: Cursor? = context.contentResolver?.query(imageUri, proj, null, null, null) val index = c?.getColumnIndexOrThrow("_data") ?: -1 c?.moveToFirst() val result = c?.getString(index).orEmpty() c?.close() val uri = imageUri.path.orEmpty() val file = File(result) val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) return MultipartBody.Part.createFormData("images", file.name, requestFile) }
3번의 과정을 선택된 사진의 수만큼 반복해서 사진들을 MultipartBody.Part로 바꿔서 List에 담아 준 뒤 서버에 전송하였습니다.
- Retrofit2로 전송하기
- 멀티파트는 POST를 이용해서 전송한다고 합니다.
- 주소도 각자 api에 맞춰서 쏴주시면 끝!
interface TransferApi { @Multipart @POST("/images") suspend fun getImageUrl( @Part images: List<MultipartBody.Part> ) }
만약에 경로가 틀리거나 permission이 없으면
이런 에러를 만나게 됩니다.
경로 확인 잘하셔야 합니다,,,,쓰앵님들
후... 공부를 더 많이 해야 할 듯합니다.
오히려 좋아!ps. 혹시나 이 글을 보시는 안드 고수님이 계시다면 ACTION_PICK을 써서 절대 경로를 받아오기 VS ACTION_GET_CONTENT를 써서 내부 저장소에 사진을 복사한 뒤 그 사진을 전송하기
이 둘 중 뭐가 더 맞는 방법인지 알려주시면 감사하겠습니다.... 귀찮으시면 키워드라도 던져주시면 감사하겠습니다. ㅠㅠ 🙇
'Android📱' 카테고리의 다른 글
[Android] Retrofit2 사용 시 헤더에 데이터 넣기! (0) 2022.10.17 [Compose] hiltViewModel을 써서 composable끼리 viewModel을 공유해보자! (4) 2022.09.20 ktlint를 사용해보자! (0) 2022.07.28 [Recycler View] View Holder (0) 2022.03.29 [Recycler View] Recycler View : adapter (0) 2022.03.29