본문 바로가기

코딩/안드로이드

Android Kotlin MVVM패턴으로 간단한 검색 앱 만들기 - 3. RxJava + Retrofit를 통한 네트워킹 및 옵저빙

이번 글에서는 Retrofit으로 네트워크 통신을 하고 그 결과값을 RxJava로 받아오는 과정을 수행해보겠습니다.



일단 Retrofit에 대해 간단히 알아보도록 하죠.




Retrofit



저도 최근에 알았는데, 레트로핏 한글 문서가 있더라구요.


http://devflow.github.io/retrofit-kr/


길지 않으니 한번 정독하셔도 됩니다.


간단하게 레트로핏을 요약하자면


public interface GitHubService {
@GET("/users/{user}/repos")
Call<Repo> listRepos(@Path("user") String user);
}


이렇게 인터페이스 형태의 서비스를 먼저 만듭니다.


자바 어노테이션으로 @Get이나 @Post 등을 적어주고, 파라미터로 라우팅 정보를 적어줍니다.


여기서 Repo는 모델 클래스입니다.


대충... 


class Repo {

String a;

String b;

.

.

}


이런식이겠고..


코틀린이라면



data class Repo(val a, val b){}


대충 이런 식으로 하면 되는데,


이 부분은 밑에서 설명하겠습니다.






그런데 여기 적힌건 자바 코드입니다.


그래서 이걸 간단하게 코틀린으로 고쳐보자면


interface GitHubService {
@GET("/users/{user}/repos")
fun listRepos(@Path("user") user:String): Call<Repo>
}


이렇게 되겠군요


이렇게만 해주고 해당 서비스를 create만 해주면 됩니다.


create해주는 방법은 아래처럼 해주시면 됩니다.



Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.build();

GitHubService service = retrofit.create(GitHubService.class);



코틀린으로 고치면.. 


val service:GitHubService = Retrofit.Builder()
.baseUrl("https://api.github.com")
.build()
.create(GitHubService::class.java);



이렇게 할 수 있겠군요.



자 그럼 이제 네트워킹을 하고자 하는 위치에서


val repos: Call<Repo> = service.listRepos("USER")


이렇게 한줄을 적어서 Call객체를 얻어오시면 됩니다.


그래서 이렇게 얻어온 Call 객체는


repos.enqueue(new Callback<Repo>() {
@Override
public void onResponse(Response<List<Contributor>> response, Retrofit retrofit) {
// handle success
}

@Override
public void onFailure(Throwable t) {
// handle failure
}
});



이렇게 하시면, 비동기적으로 네트워킹을 수행할 수 있습니다.



그런데 제가 지금 Call 객체 설명을 정말 대충 했는데, 왜냐하면 저희는 Call을 쓰지않고 RxJava를 쓸 것이기 때문입니다.



그럼 RxJava를 적용하고, Service를 얻어오는 부분에서부터 다시 해봅시다.






RxJava




솔직히 저같은 초보에게... RxJava는 만만하지 않습니다.


게다가 RxJava에서 쓰는 객체만 해도 Single이나 Observable, Publish Subject 등등.. 여러개를 쓰기 때문에 


여기서 다 설명하기에는 배보다 배꼽이 더 커져버리지 않을까, 하는 생각이 듭니다.


그래서 여기에서는 딱 지금 필요한것만 최소한으로 설명해보겠습니다.



먼저 RxJava의 공식 홈페이지입니다.


http://reactivex.io/


RxJava 뿐만아니고 Rx 시리즈를 만든 ReactiveX 홈페이지입니다.


한번 대충 훑어보셔도 좋을듯 합니다.




이제 gradle을 추가하죠.


// rxjava
implementation "io.reactivex.rxjava2:rxjava:2.2.0"
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"


쏙 넣어줍니다.



그다음, 위에서 만든 서비스 인터페이스를 4글자만 바꿔보겠습니다.


interface GitHubService {
@GET("/users/{user}/repos")
fun listRepos(@Path("user") user:String): Single<Repo>
}


뭐가 바뀌었을까요?


바로 Call이 Single로 바뀌었습니다!


나머지는 그대로죠.


그럼 저희가 지금 쓴 RxJava.Single이 무엇인지.. 에 대해 설명하기 전에 일단 코드부터 더 써보도록 하죠


이 인터페이스를 아까처럼 create해봅시다.


val service:GitHubService = Retrofit.Builder()
.baseUrl("https://api.github.com")
.build()
.create(GitHubService::class.java);


바뀐게 없습니다!


그 다음, 사용해 봅시다.



service.listRepos("USER")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
// handle success
}, {
// handle fail
}))


갑자기 이상한게 생겼네요.


subscribeOn, observeOn, subscribe 라는걸 순서대로 실행합니다.


이에 대해 아주 간단하게 설명하기 위해, RxJava의 개념부터 먼저 말씀드려야 할것 같군요.




RxJava는 간단히 말해서


"발행" 과 "구독" 입니다.


누군가는 데이터의 강에 데이터를 흘려보내고


누군가는 데이터의 강에서 데이터를 줍습니다.


전자를 발행이라고 하고


후자를 구독이라고 합니다.



그럼 지금과 같은 네트워킹에서는  


저희가 만든 레트로핏 서비스는 Single을 통해 Repo라는 데이터를 "발행" 하고,


그러한 데이터를 처리하고 싶어하는 곳에서는 그 데이터를 "구독" 해야겠죠.



그것을 위한 코드가 subscribeOn, observeOn, subscribe 입니다.


먼저 subscribeOn은 "데이터를 강에 흘려보내는 스레드(스케줄러)를 지정" 하는 작업입니다.


정확하게는 데이터를 발행(연산)하는 스케줄러를 지정하는 것이라고 할 수 있습니다.


지금 네트워킹작업은 IO작업이니까 Schedulers.io()로 설정해주었습니다.


IO 스케쥴러 말고도 메인스케쥴러, 뉴 스케쥴러등이 있습니다.



그 후에 observeOn으로 "데이터를 줍는 스레드(스케줄러)를 지정"합니다.


즉, 데이터를 구독하는 스케줄러를 지정하는 것입니다.


이건 이제 AndroidSchedulers.mainThread()로 지정해주었습니다.


메인스레드인 이유는, 안드로이드 UI작업을 해주기 위해서 입니다.



이 스케줄러에 관해서는 


http://reactivex.io/documentation/ko/scheduler.html


여기를 참고하시면 더욱 자세하게 아실 수 있습니다(한글).



이제 네트워킹을 통해서 데이터를 강에 뿌렸으니 주워서 써야겠죠.


그러한 부분이 subscribe() 입니다.


Kotlin적으로 람다 두개를 전달해주고 있습니다.


첫번째 람다는 통신이 성공했을 때, 두번째 람다는 통신이 실패했을 때 실행됩니다.


이들 람다에서는 it으로 데이터를 얻어올 수 있습니다.


예를들어,


service.listRepos("USER")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
name_text_view = it.name
}, {
Log.d(TAG, "ERROR message : ${it.message}")
}))



이렇게 할 수 있겠죠.



자 그럼 미뤄놨던 Single에 대한 설명을 짧게 해보겠습니다.


간단하게 싱글은 '싱글' 입니다.


데이터를 한번 뿌린다는 뜻이죠.


REST API로 호출한 데이터는 서버로부터 딱 한번 받고 끝이기 때문에 Single을 이용하게 됩니다.


그 외에 Observable과 Subject등등이 있습니다. 그 중에서 Publish Subject는 소켓 통신이나 여러가지 상황에서 쓰면 좋기 때문에 한번 알아두시면 좋을듯 합니다





자.. 여기까지 RxJava와 Retrofit으로 데이터를 얻어와 보았습니다.



이제 거의 마지막으로 아까 미뤄놨던 Repo 클래스에 대해 써보겠습니다.








Data


지금까지 계속 사용한 Repo클래스를 Kotlin으로 딱 한줄로 짜보겠습니다.


일단 서버에서 Json객체로 


{

"name" : "이름",

"age" : 5

}


과 같이 응답이 온다고 가정합시다.


그러면 Kotlin으로 아주 간단하게 다음과 같이 짜면 됩니다.



data class Repo(val name:String, val age:Int)



클래스의 내용물은 어디갔냐구요? Getter, Setter, toString은 어디갔냐구요?


그건 바로 data 라는 명령어가 다 알아서 해줍니다.


코틀린 만세!


kotlin의 data는 toString, hashcode, equals를 자동생성해줍니다.


그리고 기본적으로 kotlin은 getter와 setter가 있죠.


kotlin의 data클래스의 유용성에 대해서는 https://tourspace.tistory.com/108  < 이 글을 함께 보시면 좋을듯 합니다.



자 그럼 이것만 하면 완성이냐.. 하면 아직 두줄 남았습니다.


아까 Retrofit.어쩌구저쩌구....create()로 서비스를 만들었었죠


그 코드에서 두줄만 추가하면 됩니다.



retrofit:Retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.build()


이걸


retrofit:Retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://api.github.com")
.build()



이렇게 해주시면 됩니다.



방금 추가한 두 줄로 인해


RxJava와 GsonConvert가 가능해졌습니다.



특히 GsonConverter덕분에 서버로부터 응답받은 json을 아무것도 하지않고 바로 data class로 바꿔먹을수 있게 되었죠.





자, 이제 마지막으로 카카오 이미지 검색을 수행해봅시다.





 


Kakao API




이제 지금까지 한 것을 토대로 저희가 쓰려는 kakao api로 바꿔봅시다


일단 외부 api를 쓰는거니까 해당 api를 쓰기 위한 key가 있어야겠죠?


https://developers.kakao.com


여기 가셔서 대충 샘플 앱 등록하고 





설정 -> 일반 가시면 REST API키가 있습니다.


저걸 이용하시면 됩니다.




발급 받으셨다면 본격적으로 카카오 이미지 검색을 수행해 봅시다.




먼저 Rest api의 명세를 한번 슬쩍 봅시다.


https://developers.kakao.com/docs/restapi/tool


여기서 보니, 



이렇게 호출하라네요



URL은 https://dapi.kakao.com/v2/search/image 이고,


header에 방금 발급받은 키를 "KakaoAK 어쩌구저쩌구키" 형태로 넣어주고


파라미터로 query, sort, page, size를 준다음 


GET을 호출하면 되는군요.





이걸 토대로 레트로핏 서비스 인터페이스를 만듭시다.


https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/model/service/KakaoSearchService.kt

(소스코드 링크)


interface KakaoSearchService {
@GET("/v2/search/image")
fun searchImage(
@Header("Authorization") auth:String,
@Query("query") query:String,
@Query("sort") sort:String,
@Query("page") page:Int,
@Query("size") size:Int
): Single<ImageSearchResponse>
}



말 그대로죠?


해더로 Authorization 넣고,


쿼리로 4개 넣어주고


/v2/search/image로 GET호출!



https://dapi.kakao.com << 이 부분은 나중에 Retrofit.build()할때 넣어줄 예정입니다.



자 그럼 응답을 받을 ImageSearchResponse 객체를 만들어 봅시다.




다시 명세를 보고 응답이 어떻게 오는지 한번 보도록 하죠




흠.. 조금 길군요


잘 모르겠으니 호출을 해서 응답 예제를 한번 볼까요?



{
"documents": [
{
"collection": "blog",
"datetime": "2017-05-19T01:10:00.000+09:00",
"display_sitename": "네이버블로그",
"doc_url": "http://blog.naver.com/************/221009090..........",
"height": 250,
"image_url": "http://postfiles1.naver.net/MjAxNzA2MDdfMj..........",
"thumbnail_url": "https://search1.kakaocdn.net/argon/130..........",
"width": 250
}
],
"meta": {
"is_end": false,
"pageable_count": 3977,
"total_count": 5675
}
}


(몇몇 내용은 가렸습니다)



아하! 대충 느낌이 오는데요?


그럼 이걸 그대로~ data class로 만들어 볼까요?




https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/model/response/ImageSearchResponse.kt

(소스코드 링크)



data class ImageSearchResponse (
var documents:ArrayList<Document>,
var meta:Meta
) {
data class Document(
var collection:String,
var thumbnail_url:String,
var image_url:String,
var width:Int,
var height:Int,
var display_sitename:String,
var doc_url:String,
var datetime:String
)
data class Meta(
var total_count:Int,
var pageable_count:Int,
var is_end:Boolean
)
}



정말 토시 하나 틀리지 않고 똑같군요...




그럼 이제 레트로핏 서비스의 작성이 끝났으니 Retrofit.build()...어쩌구저쩌구..create() 부분을 만들어 볼까요?


Retrofit.Builder()
.baseUrl("https://dapi.kakao.com")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(KakaoSearchService::class.java)


아까 한대로, baseUrl에 url넣어주고, RxJava 설정해주고, GsonCoverter 설정해주고, create()!


자.. 그런데, 여기서 한가지 재밌는 생각이 듭니다.



이걸 Koin을 통해 DI해주면 더 재밌지않을까?




그렇다면 이렇게 하면 됩니다!



https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/di/MyModule.kt


var retrofitPart = module {
single<KakaoSearchService> {
Retrofit.Builder()
.baseUrl("https://dapi.kakao.com")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(KakaoSearchService::class.java)
}
}
.
.
.
var myDiModule = listOf(retrofitPart, . . . )




이렇게 하면 다른 곳에서 get()이나 by inject()를 통해 원하는 서비스를 얻어올 수 있죠.


실제로 제 코드에서는 DataModelImpl에서 생성자의 인자로 서비스를 얻어옵니다.




그럼 DataModelImpl를 살짝 보면


https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/model/DataModelImpl.kt


class DataModelImpl(private val service:KakaoSearchService):DataModel{

private val KAKAO_APP_KEY = "YOUR_APP_KEY"

override fun getData(query:String, sort:KakaoSearchSortEnum, page:Int, size:Int): Single<ImageSearchResponse> {
return service.searchImage(auth = "KakaoAK $KAKAO_APP_KEY", query = query, sort = sort.sort, page = page, size = size)
}
}



간단하게 Single<>을 반환해주는 역할을 하고 있습니다.


그 와중에 auth를 그냥 APP_KEY가 아니라 prefix로 "KakaoAK"를 붙여주고 있습니다.





이제 이것을 이용하는 뷰모델을 한번 보도록 하죠.



https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/viewmodel/MainViewModel.kt


fun getImageSearch(query: String, page:Int, size:Int) {
addDisposable(model.getData(query, KakaoSearchSortEnum.Accuracy, page, size)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
it.run {
if (documents.size > 0) {
_imageSearchResponseLiveData.postValue(this)
}
}
}, {
Log.d(TAG, "response error, message : ${it.message}")
}))
}



model.getData()를 통해 데이터를 쭉 얻어오고


subscribeOn과 observeOn을 수행한 다음


subscribe()로 데이터를 처리합니다.


데이터의 처리는 간단하게 it.run{}으로 코틀린스럽게 코딩하고 있습니다.


그 이후 document의 사이즈가 0보다 크면 LiveData에 post해주고있습니다.


네? 갑자기 LiveData요?


일단, 그냥 데이터를 처리한다고만 생각하시고 넘어갑시다.


그리고 다음 글에서 LiveData에 대해 설명하겠습니다.


여튼 이렇게 데이터를 받아서 처리하시면 됩니다!

 

간단하게 Log.d("TAG", "document : ${it.documents}") 로 적어주시면 검색 결과를 로그로 보실 수 있습니다.


data클래스가 알아서 toString을 만들어 주었으니 ${it.documents}라고 적어도 documents의 내용물이 적당히 이쁘게 나오는 것을 보실 수 있습니다.





자 그럼, addDisposable에 대해 마지막으로 설명하고 글을 마치도록 하겠습니다.







addDisposable, compositeDisposable.clear()



이 부분은 BaseKotlinViewModel을 보시면 있습니다.


https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/base/BaseKotlinViewModel.kt


open class BaseKotlinViewModel : ViewModel() {
.
.
.
/**
* RxJava 의 observing을 위한 부분.
* addDisposable을 이용하여 추가하기만 하면 된다
*/
private val compositeDisposable = CompositeDisposable()

fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}

override fun onCleared() {
compositeDisposable.clear()
super.onCleared()
}
.
.
.
}


보시면 addDisposable을 통해 compositeDisposable라는 곳에 뭔가를 추가하고, 뷰모델이 사라질 때(onCleared) 그걸 다 지우는걸 보실수 있습니다.


간단하게 말해서, Observing을 계속하면 메모리 누수가 생기기 때문에 데이터의 구독을 시작하면서 compositeDosposable에 추가하고,


Observing을 그만두게 될 때(뷰모델이 사라질 때 == 뷰가 사라질 때) compositeDisposable을 비워줌으로서 메모리 누수를 방지하는 작업입니다.









자, 이제 "RxJava와 Retrofit을 통한 네트워킹 구현"에 대한 글이 끝났습니다.



조금 글이 길었지만 여기까지 읽어주셔서 감사드립니다.


저도 초보라 오류가 많을 수 있는데, 오류에 대해 말씀해주시면 그 또한 감사드리겠습니다.





저는 Rxjava를 처음 접할 때, 당장 쓰고싶은건 single인데, 구글신은 RxJava 전체를 가르쳐주셔서 머리를 싸맸었습니다.


저같이 빨리 써먹어보고 흥미부터 돋구고 싶으신 분들에게 도움이 되셨으면 좋겠습니다.



다음 글은 Livedata와 Databinding에 대해 다뤄볼 예정입니다.