이미지 파일 서버로 보내기

seunghee song·2024년 3월 22일
post-thumbnail

바야흐로 11월 마감에 쫓겨 개발을 하다 게시글과 이미지 첨부 후 해당 내용을 모두 서버에 보내줘야 했다. String값으로 지정된 데이터들은 정상적으로 전송이 되었는데 이미지만 업로드가 안되는 문제가 발생했다..

마감에 쫓겨 일단 안하고 넘어갔는데 마음 한켠에 불편한 압정으로 남아있었다.. 이것만은 무조건 해결해보겠다 다짐했는데 마침내 해결하게 되었다!! 해결 과정에서 많은 자료들을 찾아 보며 생각보다 명확한 자료가 없어서 어려움을 겪어서 기록해보고자 한다!

Request body 확인

우리 게시글 작성 api는 imageList를 비롯해 다른 여러 값들도 요청해야한다. 이미지 리스트는 multipartList 형식이다.
api 요청 코드를 작성할때 이렇게 File을 보내야 할 경우 @Multipart 어노테이션을 사용해서 File을 보낼 것임을 명시해준다.

@Multipart
    @POST("/api/articles")
    suspend fun setTownDetail(
        @Header("Authorization") token:String,
        @Part imageList: List<MultipartBody.Part>,
        @PartMap data: HashMap<String, RequestBody>
    ):ApiResult<CommentWritingResponse>

이미지 리스트 이외에 다른 데이터들은 @PartMap을 사용해 HashMap형태로 묶어서 전달하고자 정의해주었다.

RepositoryImpl

override suspend fun setTownDetail(
        token: String,
        title: String,
        content: String,
        articleTag: String,
        imageList: List<MultipartBody.Part>,
        longtitude: Double,
        latitude: Double,
        regionAgreementCheck: Boolean
    )
    : ApiResult<CommentWritingResponse> {

       val map = HashMap<String, RequestBody>()
        var title = RequestBody.create("text/plain".toMediaTypeOrNull(), title)
        var content = RequestBody.create("text/plain".toMediaTypeOrNull(), content)
        var articleTag = RequestBody.create("text/plain".toMediaTypeOrNull(), articleTag)
        var longitude = RequestBody.create("text/plain".toMediaTypeOrNull(), longtitude.toString())
        var latitude = RequestBody.create("text/plain".toMediaTypeOrNull(), latitude.toString())
        var regionAgreementCheck = RequestBody.create("text/plain".toMediaTypeOrNull(), regionAgreementCheck.toString())

        map["title"] = title
        map["content"] = content
        map["articleTag"] = articleTag
        map["longitude"] = longitude
        map["latitude"] = latitude
        map["regionAgreementCheck"] = regionAgreementCheck

        return service.setTownDetail(token,imageList, map)

    }

다음으로 Repository 실구현체를 정의해준다. 이미지 리스트를 제외한 다른 데이터들은 각각 RequestBody 객체로 변환하고 이를 map에 저장한다. 그리고 MIME 타입 "text/plain"과 함께 각 값을 RequestBody로 변환한다. 마지막으로 imageList와 map에 저장된 모든 데이터를 서버로 전송한다.

Repository, Usecase, ViewModel에서는 특별한 것이 없으므로 설명을 생략하겠다.

Fragment에서의 파일형식 변환

이 부분이 가장 중요하다!! 갤러리에서 선택한 이미지를 잘못된 데이터 형식으로 변환해주어서 지금까지 오류가 발생했던 것이다.

해결과정

첫번째 시도: 갤러리로부터 얻은 이미지 uri를 File()을 사용해서 File 객체를 직접 생성해서 multipart로 변환하려고 하였다.

오류발생: 안드로이드 10 이상에서는 직접적인 파일경로 접근 대신 URI를 통한 접근이 권장된다. scoped storage 정책으로 파일 시스템에 직접 접근이 제한된다.

두번째 시도: 갤러리로부터 이미지 uri를 받아 contentResolver 사용해서 InputStream을 얻는다. 이 InputStream을 사용해 임시파일을 생성 후 그 파일을 다시 MultiPartBody.Part로 변환한다.

잠깐 ContentResolver란?

콘텐트 프로바이더로부터 데이터를 가져오는 것이 contentResolver이다. 연락처, 갤러리 등 기본 데이터 이용하는 용도로 가장 많이 사용된다.

이를 이용해서 InputStream을 얻는 과정을 거쳐야하는데 이를 얻는 과정은 주로 외부 소스에서 데이터를 읽어오는데 사용된다.

따라서 우리는 사용자의 민감한 파일에 접근을 할때에는 ContentResolver을 사용해야하고 InputStream을 통해 데이터에 접근해야한다. 이를 사용하면 데이터를 바이트 단위로 읽을 수 있고 파일의 내용을 읽거나 다른 형식으로 변환, 임시파일 생성 등의 작업을 수행할 수 있다.

아래 코드는 uri리스트를 MultipartBody 형식 데이터로 변환하는 함수 코드이다.

 private fun UrisToMultipartBody(
        uris : List<String>,
        contentResolver: ContentResolver
    ): List<MultipartBody.Part>{
        val multiPartList = mutableListOf<MultipartBody.Part>()
        uris.forEach { uriString->
            val uri = Uri.parse(uriString)
            try{
                contentResolver.openInputStream(uri)?.use{inputStream ->
                    val tempFile = File.createTempFile("upload_", ".jpg", context?.cacheDir).apply {
                        deleteOnExit()
                    }
                    inputStream.copyTo(tempFile.outputStream())
                    val requestFile = tempFile.asRequestBody("image/*".toMediaTypeOrNull())
                    val result = MultipartBody.Part.createFormData("imageList", tempFile.name, requestFile)
                    multiPartList.add(result)
                }
            }catch (e:Exception){
                Log.d("UrisToMultipartBody", "$uriString")
                null
            }
        }
        return multiPartList
    }

주의할 점! ' val result = MultipartBody.Part.createFormData("imageList", tempFile.name, requestFile)' 이 부분에서 api 명세서에 정의되어 있는 데이터 이름 형식과 같아야 한다. 우리 앱의 api명세서에서는 imageList라는 이름으로 request를 보내줘야 하기 때문에 위와 같이 작성하였다.

이렇게 해서 나의 갤러리의 사진이 서버에 올라가는 감격스러운 장면을 볼 수 있었다.

끝으로 몇달 전에 작성했던 코드도 굉장히 엉켜있고 클린하지 못하다는 것을 느꼈다. 클린 또 클린하게 짜도록 노력하자...

profile
안드로이드 개발자

1개의 댓글

comment-user-thumbnail
2024년 3월 24일

이 글을 보고 10년간 묵혀두었던 사진이 구글드라이브에 올라갔어요

답글 달기