📌 Intro
이전 글에서는 다른 블로그에서 학습한 내용을 정리하면서 Retrofit의 기본적인 개념에 대해 공부했다면 이번 글에서는 종합설계 과목을 진행하면서 직접 적용해본 내용을 바탕으로 글을 작성하려고 한다. 나름 자세하게 작성한다고 했는데 나중에 봤을 때 무슨 말인지 알아볼 수 있을지 의문이다.
📌 Permssion
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
서버와 통신하기 위해서는 INTERNET 퍼미션이 필요하고 휴대폰 내의 저장소에 접근하기 위한 퍼미션이 필요하다.
📌 Server
서버에서 요구하는 정보는 영상(video), 영상의 썸네일(thumbnail), 영상 제목(titleName)이다. 그리고 성공적으로 데이터를 전송한 경우 다음과 같은 결과 값이 반환된다.
{
"code": "string",
"data": "string",
"isSuccess": true,
"message": "string"
}
따라서 클라이언트에서는 video, thumbnail, titleName을 파라미터로 가진 POST API와 POST를 요청한 뒤 반환받을 클래스를 만들어줘야 한다.
위 사진은 Postman에서 확인해본 결과다.
📌 Client API
클라이언트에서 생성한 API는 다음과 같다.
@Multipart
@POST("uploadV3") // 1
fun sendVideo(
@Part thumbnail: MultipartBody.Part,// 2
@Part titleName: MultipartBody.Part,// 3
@Part video: MultipartBody.Part // 4
): Call<Data.PostTest> // 5
클라이언트에서 생성한 클래스는 다음과 같다. 내용은 주석의 번호에 맞게 설명하도록 하겠다.
// POST 사용 클래스
data class PostTest( // 6
val isSuccess: Boolean,
val message: String,
val code: String,
val data: String
)
- POST형태로 보낼 것이고 호스트 주소가 BASE URL이후 uploadV3 이기 때문에 이 값을 넣어준다.
- 서버에서 요구하는 파라미터로 thumbnail이 있고 서버에 이 key값에 대한 value임을 알려주기 위해 변수명이 같아야 한다. 파일은 MultipartBody.part의 형태로 보내야 하기 때문에 자료형을 MultipartBody.part로 지정해주었다.
- 서버에서 요구하는 파라미터로 titleName이 있고 서버에 이 key값에 대한 value임을 알려주기 위해 변수명이 같아야 한다. 파일명은 String으로 보내도 되는것이 아닌가 하는 의문이 들지만 왜인지 모르게 String으로 해서 보내면 서버에서 값을 받지 못하는 문제가 발생했다. 그래서 혹시나 하는 마음에 MultipartBody.Part 형태로 보내주었더니 에러가 발생하지 않았다. 이 부분에 대해서는 나중에 더 알아보고 수정해야 할 것 같다.
- 서버에서 요구하는 파라미터로 video가 있고 서버에 이 key값에 대한 value임을 알려주기 위해 변수명이 같아야 한다. 파일은 MultipartBody.part의 형태로 보내야 하기 때문에 자료형을 MultipartBody.part로 지정해주었다.
- 성공적인 POST이후 서버에서 반환해주는 값이 Boolean값 하나, String값 3개 이기 때문에 이에 맞는 클래스를 Call<> 안에 넣어준 것이다.
- 서버에서 반환해주는 값과 같은 자료형과 변수명을 가진 클래스를 만들어준다. 만약 변수명을 일치시켜주지 않는다면 해당 값에 저장되어있는 데이터를 받아올 수 없다.(다른 변수명을 사용하고 싶다면 @SerializedName()을 사용하는 방법이 있다.)
📌 Mainactivity.kt
object RetrofitClass {
private var BASE_URL = "서버 URL"
private val retrofit = Retrofit.Builder()
.baseUrl(this.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val inquire = retrofit.create(Interact::class.java)
val api
get() = inquire
}
Retrofit 클래스는 여러번 생성할 필요가 없기 때문에 object를 통해 싱글톤 객체로 생성해준다. 서버 URL자리에 알맞는 서버 BASE URL을 넣어주도록 한다.
// 1.
val videoUri = "서버로 전송할 파일의 상대경로"
// 2. 절대 경로로 변환
private fun makeAbsolutePath(path: Uri?, context: Context): String {
val proj: Array<String> = arrayOf(MediaStore.Images.Media.DATA)
val c: Cursor? = context.contentResolver.query(path!!, proj, null, null, null)
c?.moveToFirst()
val index = c?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val result = c?.getString(index!!)
c?.close()
return result!!
}
// 3. 제목 MultipartBody 반환
private fun makeTitleRequestBody(): MultipartBody.Part {
val videoTitle = "비디오 제목"
return MultipartBody.Part.createFormData("titleName", videoTitle)
}
// 4. 비디오 MultipartBody 반환
private fun makeVideoRequestBody(uri: Uri?): MultipartBody.Part {
val file = File(makeAbsolutePath(uri, requireActivity()))
val requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file)
return MultipartBody.Part.createFormData("video", file.name, requestFile)
}
// 5. 썸네일 MultipartBody 반환
private fun makeThumbnailRequestBody(): MultipartBody.Part {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(makeAbsolutePath(videoUri, requireContext()))
val bitmap = retriever.getFrameAtTime(1)!!
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
val byteArray = stream.toByteArray()
val requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), byteArray)
return MultipartBody.Part.createFormData("thumbnail", "hello.jpg", requestFile)
}
// 6. sendVideo API 호출
private fun callSendVideo() {
val service = RetrofitClass.api.sendVideo(
makeThumbnailRequestBody(),
makeTitleRequestBody(),
makeVideoRequestBody(videoUri)
)
service.enqueue(object : Callback<Data.PostTest> {
override fun onResponse(call: Call<Data.PostTest>, response: Response<Data.PostTest>) {
if (response.isSuccessful) { // 성공시
Log.d(tag, "통신 성공")
Log.d(tag, response.body().toString())
} else { // 실패시
Log.d(tag, "통신 실패")
}
}
override fun onFailure(call: Call<Data.PostTest>, t: Throwable) {
Log.d(tag, t.message.toString())
}
})
}
1. 서버로 전송할 영상의 상대경로를 저장한다
2. 상대경로 uri를 이용하여 절대경로로 변경해주는 역할을 한다. 절대경로로 변경해주는 이유는 파일 형태로 만들어주기 위함이다.
3. 영상 제목을 MultipartBody.part로 변경하여 반환하는 역할을 한다. createFormData()에 들어가는 파라미터는 다음과 같은 의미를 가진다.
1) 서버에서 받는 키값 String
2) value값 String
4. 영상을 MultipartBody.part로 변경하여 반환하는 역할을 한다. createFormData()에 들어가는 파라미터는 다음과 같은 의미를 가진다.
1) 서버에서 받는 키값 String
2) 파일 이름 String
3) 파일 경로를 가지는 RequestBody 객체
5. 썸네일을 MultipartBody.part로 변경하여 반환하는 역할을 한다. createFormData()에 들어가는 파라미터는 다음과 같은 의미를 가진다.
1) 서버에서 받는 키값 String
2) 파일 이름 String
3) 파일 경로를 가지는 RequestBody 객체
6. 앞에서 만들었던 sendVideo API를 호출한다. 이 때 파라미터로 썸네일의 MultipartBody.part, 제목의 MultipartBody.part, 영상의 MultipartBody.part 를 넘겨준다. sendVideo API의 반환 값이 앞에서 생성한 데이터 클래스 형태이기 때문에 Call<>안에 Data.PostTest를 넣어준 것이다.(Data.kt 이라고 새로 만든 파일안에 PostTest를 만들었기 때문에 Data.PostTest다.)
📌 정리
실제로 적용해보지 않아서 겪었던 문제들이 많았다.
- titleName이 결국 String 값을 넘겨주어야 하는데 API에서 MultipartBody.Part 형태로 해주어야 했던 점
- createFormData()에 들어가는 파라미터로 서버에서 받는 키 값을 넣어야했던 점
- API를 만들 때 서버에서 사용하는 변수명과 같은 변수명을 사용해야 했던 점
- 반환 클래스 생성시 서버에서 반환해주는 변수명과 같은 변수명을 사용해야 했던 점
이렇게 어려웠던(?) 부분들을 정리해두었으니 다음번에 진행할 때는 조금 더 수월하게 할 수 있지 않을까 생각된다. 역시 직접 적용하면서 개발하는게 가장 재미있는 것 같다. 서버랑 연동하는 API를 만들고 동작하는 것을 확인하니까 정말 재미있었다.
📌 참고
[1] https://juju-marooooo.tistory.com/38
[2] https://machine-woong.tistory.com/171
[3] https://velog.io/@dev_thk28/Android-Retrofit2-Multipart사용하기-Java
[4] https://huiveloper.tistory.com/13
[5] https://futurestud.io/tutorials/retrofit-2-how-to-upload-files-to-server