Note: (updated: June 2023)
12 June 2020 - 라이브러리 관련 내용이 수정 되었습니다.
이 연재는 'Do it Kotlin Programming'의 후속 과정으로 진행합니다. 입문자는 먼저 책을 읽어보세요!
완성도 높은 앱을 만들어 내기 위해서는 모든 기능을 직접 설계하는 것보다 서드파티 라이브러리를 활용하는 것이 좋습니다. 경우에 따라서는 상용으로 제공되는 라이브러리도 있으나 대부분 오픈소스 라이브러리 만으로도 충분히 좋은 앱을 만들 수 있습니다. 이 프로젝트에서는 다음과 같은 기본 라이브러리와 서드파티 라이브러리를 활용할 것입니다.
이번 절에서는 코틀린에서 기본제공되는 라이브러리 이외의 위의 라이브러리를 소개 하겠습니다.
HTTP 관련 요청을 구현하기 위해 연결이나 캐싱, 요청, 스레딩과 파싱, 에러처리 등 만들어야 할 루틴들이 너무나 많습니다. Retrofit은 HTTP의 REST API 구현을 위한 인기 있는 라이브러리로 Square사의 오픈 소스 라이브러리 중 하나로, 앞서 언급한 기능들을 제공해 해야할 일을 많이 줄여주죠.
Square사는 otto, dagger, Picasso, OkHTTP와 같은 라이브러리도 배포하고 있습니다.
REST는 Representational State Transfer의 약자로 네트워크상 클라이언트의 통신방식을 말하며 HTTP의 GET, POST, PUT, DELETE와 같은 요청을 처리하는 방법을 제공하는 것입니다. 클라이언트의 응답에 대한 처리로 xml, json, text, rss등을 지원하는데 그 중 json 방식이 인기가 있죠.
먼저 라이브러리를 사용하기 위한 Gradle 파일에 다음과 같이 지정할 수 있습니다.
implementation 'com.squareup.retrofit2:retrofit:<버전명>'
Retrofit은 Java 8 이상 또는 안드로이드 API Level 21이상에서 사용할 수 있습니다.
사이트에서 소개된 코드는 자바로 작성되었지만 코틀린에서도 사용 가능합니다. 한번 살펴봅시다.
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
이렇게 인터페이스를 만들고 @GET
어노테이션을 만드는 것으로 GitHub API중에서 user와 관련된 부분을 읽어올 준비가 됩니다. @Path
는 경로상의 {...}를 대체할 수 있습니다.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
Retrofit의 빌더 메서드인 baseUrl()을 통해서 URL을 초기화 할 수 있습니다. 이때 생성된 retrofit
객체는 앞서 지정된 GitHubService 인터페이스를 통해 하나의 서비스 요청이 생성됩니다. 이 요청을 받은 서버는 XML, JSON등 다양한 형태로 응답할 수 있습니다. 이 응답에 대응하려면 다양한 컨버터들이 있어야 합니다. Retrofit은 다음과 같은 컨버터를 제공합니다.
Gson은 자바객체와 JSON을 서로 변환 할 수 있는 구글에서 제공하는 라이브러리 입니다. Jackson은 JSON뿐만 아니라 다양한 자료형을 제공합니다. 그 밖에도 기타 사용하는 라이브러리나 목적에 따라 컨버터를 선택할 수 있습니다. 예를 들어 Gson을 사용하기 위해서는 build.gradle에 다음과 같이 추가하고 사용합니다.
implementation 'com.squareup.retrofit2:converter-gson:<버전명>'
그리고 retrofit 초기화 코드에 addConverterFactory()을 다음과 같이 추가합니다.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
보통 웹서비스는 비동기적으로 응답을 처리해야 합니다. 따라서 이에 필요한 추가 어댑터인 RxJava를 사용하는 경우에 build.gradle에 다음과 같이 추가합니다.
implementation 'com.squareup.retrofit2:adapter-rxjava:<버전명>' // RxJava 1 또는 implementation 'com.squareup.retrofit2:adapter-rxjava2:<버전명>' // RxJava 2 또는 implementation 'com.squareup.retrofit2:adapter-rxjava3:<버전명>' // RxJava 3
그 다음 인터페이스에는 다음과 같이 Observable<Repo>
을 정의 합니다. Kotlin에서 정의하면 다음과 같습니다.
@GET("/search/users?")
fun searchUser(
@Query(value = "q", encoded = true) userKeyword: String,
@Query("page") page: Int,
@Query("per_page") perPage: Int): Observable<GitHubUserSearchResponse>
따라서 초기화 시에는 추가 어댑터 설정을 다음과 같이 넣어 줍니다.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 혹은 RxJava2CallAdapterFactory, RxJava3Call...
.addConverterFactory(GsonConverterFactory.create())
.build();
이후에는 RxJava나 RxAndroid를 다음과 같이 build.gradle에 추가하고 나머지 루틴을 개발 합니다.
implementation "io.reactivex.rxjava3:rxandroid:<버전명>" // June 2020 기준: 3.0.0 implementation "io.reactivex.rxjava3:rxjava:<버전명>" // June 2020 기준: 3.0.0
다음 사이트에서 추가 정보를 확인할 수 있습니다.
https://github.com/square/retrofit/tree/master/retrofit-adapters/rxjava2 https://github.com/ReactiveX/RxAndroid
Retrofit 2.6.0 이상에서는 내장된 suspend를 통해서 코틀린의 코루틴과 연동할 수 있게 되었습니다. HTTP 요청의 비동기 표현을 다음과 같이 작성할 수 있습니다.
@GET("users/{id}") suspend fun user(@Path("id") id: Long): User
이것은 내부적으로 fun user(...): Call<User>
로 정의되고 Call.enqueue
를 호출합니다. Retrofit 2.9.0에서는 RxJava 3어댑터의 create()
메서드는 기본적으로 비동기 HTTP 요청을 생성합니다. 동기 요청에 대해서는 createSynchronous()
를 사용해 구성하며 스케줄러 상에서 사용하기 위해 createWithScheduler()
를 이용할 수 있습니다.
Reactive 프로그래밍으로 RxJava가 많이 사용되는데 Reactive 프로그래밍이란 데이터를 비동기 및 이벤트 기반 방식으로 처리해 효율성을 높여주고자 나온 자바 VM을 위한 라이브러리입니다. 다음 사이트에서 추가 정보를 확인할 수 있습니다.
https://github.com/ReactiveX/RxJava
통신에서 일반적인 비동기 데이터 처리가 끝날 때까지 스레드를 대기시키거나 콜백을 받아서 처리하면 불필요한 리소스 사용이 발생하게 됩니다. 반면 메시징 기반의 Reactive 프로그래밍에서는 필요한 경우에만 스레드를 생성 후 메시지 형태로 전달하기 때문에 더 효율적으로 컴퓨팅 리소스를 사용할 수 있습니다. 물론, 우리는 코틀린의 코루틴을 통해서 비동기 프로그래밍을 손쉽게 적용할 수 있다는 것을 알고 있습니다. 코루틴은 이제 막 안정되기 시작한 라이브러리이고 RxJava는 비교적 오랜 시간에 걸쳐 안정되고 인기 있는 라이브러리이기 때문에 기존의 프로젝트가 RxJava를 사용했을 경우 읽고 이해하기 위해 알아둘 필요가 있습니다.
RxJava는 스트리밍 형태의 데이터 기반 가공, 변형등의 동작을 제공해주는 라이브러리로써 Reactive Extension 에 대한 Java 구현체입니다. RxAndroid와 함께 사용하면 Android 의 스레드 기법을 사용하지 않고도 손쉽게 멀티 스레드 프로그래밍을 구현할 수 있습니다. 코틀린에서 사용하기 위해서 확장된 RxKotlin도 있습니다. build.gradle에 다음과 같이 지정합니다.
implementation 'io.reactivex.rxjava3:rxandroid:3.x.x' // Sep 2023: 3.1.8 implementation 'io.reactivex.rxjava3:rxjava:3.x.x'
RxJava를 사용하려면 Observer패턴에 대한 이해가 필요 합니다.
Observer 패턴은 객체의 상태 변화를 관찰하는 관찰자들인 Observer들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접, 목록의 각 Observer들에게 통지하도록 하는 디자인 패턴 입니다. 발행/구독 모델로 알려져 있기도 합니다. 이벤트 처리 시스템이나 GUI 를 설계할 때 사용되는 패턴이기도 합니다.
위 그림에서 Subject 는 이벤트를 발생시키는 주체입니다. RxJava 에서는 Observable 또는 Subject라는 이름으로 표현이 됩니다. Subject 에서 발생되는 이벤트들은 그 Subject 에 관심 있다고 등록한 Observer 들에게 전달됩니다. 여기서 Observer 는 RxJava 에서는 Subscriber 라는 이름으로 표현이 됩니다.
기본적으로 관찰자(Observer) 혹은 구독자(Subscriber)인 o1, o2는 관심주제(Subject) s1에 붙임(attach) 혹은 등록(register) 혹은 구독(subscribe)이라는 과정을 진행 합니다. 이 후에 관심주제 s1에 이벤트가 발생하면 통보(notify)하게 되고 관찰자들은 상태를 갱신(update) 할 수 있게 됩니다.
그러면 RxJava에서 사용하는 용어 기준으로 다시 한번 정리하면 다음과 같습니다.
이벤트가 발생하면 onNext()
, 이벤트의 종료는 onCompleted()
, 에러가 발생한 경우는 onError()
이 처리합니다. 간단한 자바 코드로 예를 들어보겠습니다.
// Observable 생성
Observable<String> o1 = Observable.just("One"); // (1)
// 구독
o1.subscribe(new Subscriber<String>() {
@Override public void onNext(String text) {
System.out.println("onNext : " + text);
}
@Override public void onCompleted() {
System.out.println("onCompleted");
}
@Override public void onError(Throwable e) {
System.out.println("onError : " + e.getMessage());
}
});
(1)번 부분인 Observable.just()
는 누군가가 구독(subscribe)을 하게 되면 "One"이라는 이벤트를 한번 발생시킵니다. 이후 onNext()
로 "One"이 전달되고 그 다음 onCompleted()
가 호출됩니다. 만일 (1)번 부분을 다음과 같이 변경하면 Iterable 요소의 순서대로 이벤트를 발생합니다.
Observable<String> observable = Observable.from(new String[] { "One", "Two", "Three" });
지속적으로 onNext를 발생하다가 onCompleted를 마지막으로 호출하고 종료됩니다.
onNext : One onNext : Two onNext : Three onCompleted
세번째로 Observable.defer()
를 사용하면 특정 함수를 실행 시킬 수 있습니다.
Observable<String> observable = Observable.defer(new Func0<Observable<String>>() {
@Override public Observable<String> call() {
System.out.println("defer function call");
return Observable.just("HelloWorld");
}
});
특정 함수를 실행하고 이 함수에 있는 Observable.just()
를 다시 반환 받아 전달하고 있습니다.
비동기 처리를 위한 새로운 스레드를 생성해 처리하고자 하는 경우에는 subscribeOn()
과 observeOn()
을 이용합니다.
...
System.out.println(Thread.currentThread().getName() + ", create observable");
Observable<String> observable = Observable.defer(new Func0<Observable<String>>() {
@Override public Observable<String> call() {
System.out.println(Thread.currentThread().getName() + ", defer function call");
return Observable.just("HelloWorld");
}
});
...
System.out.println(Thread.currentThread().getName() + ", do subscribe");
observable
.subscribeOn(Schedulers.computation()) // 연산을 위한 스레드에서 defer 함수가 실행된다
.observeOn(Schedulers.newThread()) // 새로운 스레드에서 Subscriber로 이벤트가 전달된다
.subscribe(new Subscriber<String>() {
@Override public void onNext(String text) {
System.out.println(Thread.currentThread().getName() + ", onNext : " + text);
}
@Override public void onCompleted() {
System.out.println(Thread.currentThread().getName() + ", onCompleted");
}
@Override public void onError(Throwable e) {
System.out.println(Thread.currentThread().getName() + ", onError : " + e.getMessage());
}
});
System.out.println(Thread.currentThread().getName() + ", after subscribe");
}
subscribeOn()은 구독이 이루어지는 스레드로 옵션인 computation()이 정의되었고 observeOn()에는 관찰자에게 전달될 때 사용하는 스레드가 지정되어 실행됩니다. 결과는 다음과 같습니다.
main, create observable main, do subscribe main, after subscribe RxComputationThreadPool-3, defer function call RxNewThreadScheduler-1, onNext : HelloWorld RxNewThreadScheduler-1, onCompleted
안드로이드의 UI스레드인 메인스레드를 관찰하기 위해서는 RxAndroid를 통해 다음과 같이 사용할 수 있습니다.
Observable.just("one", "two", "three", "four", "five")
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* an Observer */);
안드로이드에는 메시지 전달역할을 하는 루퍼(Looper)라는 개념이 있다고 했었습니다. 메시지큐에서 메시지를 스레드에 전달해 처리하는 역할을 하죠. 그런 루퍼를 관찰하기 위해서는 다음과 같이 지정할 수 있습니다.
Looper backgroundLooper = // ...
Observable.just("one", "two", "three", "four", "five")
.observeOn(AndroidSchedulers.from(backgroundLooper))
.subscribe(/* an Observer */)
RxKotlin 확장을 사용하면 코틀린 문법으로 자바의 보일러플레이트한 코드를 확 줄일 수 있습니다.
https://github.com/ReactiveX/RxKotlin
이 사이트에서 소개된 컬렉션에 사용된 예제를 확인해 봅시다.
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.rxkotlin.toObservable
fun main(args: Array<String>) {
val list = listOf("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
list.toObservable() // 컬렉션의 함수 확장
.filter { it.length >= 5 }
.subscribeBy( // 이름있는 인자들 통해 람다식을 지정
onNext = { println(it) },
onError = { it.printStackTrace() },
onComplete = { println("Done!") }
)
}
onNext, onError, onComplete는 람다 함수를 지정 받게 되어서 코드가 매우 간략해졌습니다.
옵저버 패턴은 발행자(Publisher 혹은Observable)가 구독자(Subscriber 혹은 Observer)에게 데이터나 이벤트를 Push(notifyObservers)하는 방식으로 전달합니다. 이 때 구독자가 처리할 수 있는 데이터나 이벤트 개수를 넘는 경우 다음과 같은 문제가 발생할 수 있습니다.
그렇다면 이것을 해결하기 위해서는 구독자가 자신이 처리할 수 있는 만큼의 데이터를 요청하는 방식으로 해결할 수 있을 것입니다. 이런 방식을 Back Pressure라고 하며 Reactive Streams는 이런한 배경에 의해 설계되었습니다. 넌블로킹, 비동기 스트림 처리 표준으로 Java 9의 java.util.concurrent패키지에 Flow라는 형태로 JDK에 포함되었습니다.
구현을 위한 인터페이스가 다음과 같이 정의되어 있습니다.
public interface Publisher<T> {
public void subscribe(Subscriber<? super T> s);
}
public interface Subscription {
public void request(long n);
public void cancel();
}
public interface Subscriber<T> {
public void onSubscribe(Subscription s);
public void onNext(T t);
public void onError(Throwable t);
public void onComplete();
}
API를 간단히 정리하면 다음과 같습니다.
기본적으로 Subscriber는 subscribe()를 통해서 Publisher에게 구독을 요청합니다. 이후 Publisher는 onSubscribe()를 사용해 Subscriber에게 Subscription을 전달합니다.
이제 Subscription은 Subscriber와 Publisher간 통신의 매개체가 됩니다. Subscriber는 Publisher에게 직접 데이터를 요청하지 않고 Subscription의 request()함수를 통해 Publisher에게 전달합니다.
이후 Publisher는 Subscription을 통해 Subscriber의 onNext에 데이터를 전달하고, 작업이 완료되면 onComplete, 에러가 발생하면 onError에 시그널을 전달해 처리하도록 합니다.
추가 정보는 다음 사이트에서 확인합니다.
https://github.com/ReactiveX/RxAndroid https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html
다음 글에서는 이미지 라이브러리에 대해서 소개하겠습니다.
"어떤 것을 완전히 알려거든 그것을 다른 이에게 가르쳐라."
- Tryon Edwards -