[연재] 코틀린 프로젝트 - 01 오픈소스 라이브러리의 이해: REST API

 

Note: (updated: June 2023)
12 June 2020 - 라이브러리 관련 내용이 수정 되었습니다. 
이 연재는 'Do it Kotlin Programming'의 후속 과정으로 진행합니다. 입문자는 먼저 책을 읽어보세요!

완성도 높은 앱을 만들어 내기 위해서는 모든 기능을 직접 설계하는 것보다 서드파티 라이브러리를 활용하는 것이 좋습니다. 경우에 따라서는 상용으로 제공되는 라이브러리도 있으나 대부분 오픈소스 라이브러리 만으로도 충분히 좋은 앱을 만들 수 있습니다. 이 프로젝트에서는 다음과 같은 기본 라이브러리와 서드파티 라이브러리를 활용할 것입니다. 

  • REST API - Retrofit: Http의 REST API를 사용하기 위해
  • 비동기 작업 - Coroutines, RxJava(RxKotlin): UI와 백그라운드 태스크를 분리하기 위해
  • 이미지 로드 - Picasso: 이미지를 로드하기 위해
  • 의존성 주입 - Dagger 2, Koin: 인젝션에 필요한 라이브러리
  • 안드로이드UI - RecyclerView: infinite scroll을 적용해 정보를 나타내기 위해
  • UI 바인드등 - Kotlin Android Extensions: xml 레이아웃을 코드에 바인드하기 위해
  • 각종 헬퍼 라이브러리 - Android Jetpack: 안드로이드 개발을 위한 각종 라이브러리

이번 절에서는 코틀린에서 기본제공되는 라이브러리 이외의 위의 라이브러리를 소개 하겠습니다. 

REST API - Retrofit

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은 다음과 같은 컨버터를 제공합니다. 

Retrofit이 제공하는 컨버터

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • JAXB: com.squareup.retrofit2:converter-jaxb
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

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와 연동

보통 웹서비스는 비동기적으로 응답을 처리해야 합니다. 따라서 이에 필요한 추가 어댑터인 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() 를 이용할 수 있습니다.

비동기작업 - RxJava

Reactive 프로그래밍으로 RxJava가 많이 사용되는데 Reactive 프로그래밍이란 데이터를 비동기 및 이벤트 기반 방식으로 처리해 효율성을 높여주고자 나온 자바 VM을 위한 라이브러리입니다. 다음 사이트에서 추가 정보를 확인할 수 있습니다.

https://github.com/ReactiveX/RxJava

통신에서 일반적인 비동기 데이터 처리가 끝날 때까지 스레드를 대기시키거나 콜백을 받아서 처리하면 불필요한 리소스 사용이 발생하게 됩니다. 반면 메시징 기반의 Reactive 프로그래밍에서는 필요한 경우에만 스레드를 생성 후 메시지 형태로 전달하기 때문에 더 효율적으로 컴퓨팅 리소스를 사용할 수 있습니다. 물론, 우리는 코틀린의 코루틴을 통해서 비동기 프로그래밍을 손쉽게 적용할 수 있다는 것을 알고 있습니다. 코루틴은 이제 막 안정되기 시작한 라이브러리이고 RxJava는 비교적 오랜 시간에 걸쳐 안정되고 인기 있는 라이브러리이기 때문에 기존의 프로젝트가 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'

 

Observer 패턴

RxJava를 사용하려면 Observer패턴에 대한 이해가 필요 합니다. 

Observer 패턴은 객체의 상태 변화를 관찰하는 관찰자들인 Observer들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접, 목록의 각 Observer들에게 통지하도록 하는 디자인 패턴 입니다. 발행/구독 모델로 알려져 있기도 합니다. 이벤트 처리 시스템이나 GUI 를 설계할 때 사용되는 패턴이기도 합니다. 


Observer 패턴의 클래스 다이어그램

위 그림에서 Subject 는 이벤트를 발생시키는 주체입니다. RxJava 에서는 Observable 또는 Subject라는 이름으로 표현이 됩니다. Subject 에서 발생되는 이벤트들은 그 Subject 에 관심 있다고 등록한 Observer 들에게 전달됩니다. 여기서 Observer 는 RxJava 에서는 Subscriber 라는 이름으로 표현이 됩니다.


Observer 패턴의 시퀀스 다이어그램

기본적으로 관찰자(Observer) 혹은 구독자(Subscriber)인 o1, o2는 관심주제(Subject) s1에 붙임(attach) 혹은 등록(register) 혹은 구독(subscribe)이라는 과정을 진행 합니다. 이 후에 관심주제 s1에 이벤트가 발생하면 통보(notify)하게 되고 관찰자들은 상태를 갱신(update) 할 수 있게 됩니다. 

그러면 RxJava에서 사용하는 용어 기준으로 다시 한번 정리하면 다음과 같습니다. 

  • 이벤트(Event): 구독자들에게 전달되는 데이터 (주로 클릭, 상태, API 응답 등)
  • 구독(Subscribe): Subscriber가 이벤트를 전달받기 위해 하는 행위(등록)
  • 관찰(Observe): RxJava에서는 Observable 컴포넌트들을 서로 연결(map())할 수 있으며 Observable 은 다른 Observable 을 관찰. Subscriber 는 구독(subscribe())을 통해서 Observable 을 관찰.

이벤트의 처리

이벤트가 발생하면 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에서의 처리

안드로이드의 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 확장의 사용

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는 람다 함수를 지정 받게 되어서 코드가 매우 간략해졌습니다. 

Reactive Streams와 등장 배경

옵저버 패턴은 발행자(Publisher 혹은Observable)가 구독자(Subscriber 혹은 Observer)에게 데이터나 이벤트를 Push(notifyObservers)하는 방식으로 전달합니다. 이 때 구독자가 처리할 수 있는 데이터나 이벤트 개수를 넘는 경우 다음과 같은 문제가 발생할 수 있습니다.

  • 구독자가 별도 버퍼를 구성할 수 있으나 금방 소모되며 큐의 크기를 넘어간 데이터는 소실된다.
  • 외부에 큐를 너무 크게 생성하면 OOM(Out of Memory)문제가 발생할 수 있다.

그렇다면 이것을 해결하기 위해서는 구독자가 자신이 처리할 수 있는 만큼의 데이터를 요청하는 방식으로 해결할 수 있을 것입니다. 이런 방식을 Back Pressure라고 하며 Reactive Streams는 이런한 배경에 의해 설계되었습니다. 넌블로킹, 비동기 스트림 처리 표준으로 Java 9의 java.util.concurrent패키지에 Flow라는 형태로 JDK에 포함되었습니다. 

Reactive Streams API

구현을 위한 인터페이스가 다음과 같이 정의되어 있습니다. 

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를 간단히 정리하면 다음과 같습니다. 

  • Publisher: Subscriber의 구독을 받기 위한 subscribe()메서드를 가진다.
  • Subscriber: 받은 데이터를 처리하기 위한 onNext, 에러 처리를 위한 onError, 작업 완료시 onComplete, 그리고 매개변수로 Subscription을 받는 onSubscribe가 있다.
  • Subscription: n개의 데이터를 요청하는 request와 구독을 취소하는 cancel을 갖는다.

기본적으로 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 

다음 글에서는 이미지 라이브러리에 대해서 소개하겠습니다.

 

 
youngdeok's picture

Language

Get in touch with us

"If you would thoroughly know anything, teach it to other."
- Tryon Edwards -