Android Pie (API 28, 9.0) 지원 사항

2018년 11월 기준으로 신규 / 업데이트 앱은 Android Oreo (API 27, 8.0 – 8.1) 을 targetSDKVersion 으로서 지원해야 되므로 최소 2019년 중반 까지는 Andorid Pie 에 대한 지원이 되어야 합니다.

현재 구글 픽셀 시리즈, 에센셜, 엑스페리아 XZ3 에 적용되었으며 LG V40 ThinQ 및 삼성
갤럭시 S10에도 적용 예정으로 발표되었습니다. 삼성 갤럭시 S8의 경우 ‘메이저 OS 2회 업데이트’ 정책에 따라 이번이 마지막
지원이 됩니다. (7.0 -> 8.0 -> 9.0)

참조 문서

Andorid Pie 에서 실행되는 모든 앱에 적용되는 지원 사항

아래 사항은 Android Pie에 구동되는 모든 앱에 해당되며, 이번 지원 사항에는 아래와 같은 주요 포인트를 가지고 있습니다.

  • 전원 관리 – 앱 대기 버킷, 배터리 세이버 개선
  • 백그라운드에서의 센서 (마이크, 카메라, 가속도계, 자이로스코프) 엑세스 제한
  • 통화 로그 액세스 제한 및 CALL_LOG 권한 그룹 신설
  • 전화번호 액세스 제한
  • 비 SDK 인터페이스에 대한 제한

이 중 개발자가 반드시 내용을 파악하고 있어야 하는 것은 전원 관리비 SDK 인터페이스에 대한 제한 이며 아래부터 중요도 순으로 내용을 설명합니다.

비 SDK 인터페이스에 대한 제한

먼저, 비 SDK 인터페이스 란 공식 Android SDK의 일부가 아닌
Java 필드와 메서드로 정의할 수 있습니다. 쉽게 말하지면, private 및 protected 접근자를 가지고 있는 필드 및
메서드라 표현할 수 있습니다. 공식 Andorid SDK의 모든 API는 Android 프레임워크의 SDK 문서에 전부 작성되어있으며, 작성되어있지 않은 API의 경우는 비 SDK 인터페이스라 볼 수 있습니다.

기존까지 비 SDK 인터페이스를 사용하려면 Class 클래스의 getDeclearedFieldgetDeclearedMethod 등의 메서드를 사용하여 각각의 FieldMethod 객체를 찾아 직접적으로 실행하는 방법이 흔히 쓰였습니다.

하지만 Android Pie에서 돌아가는 앱은 이러한 방법을 사용했을 때 정확한 객체를 반환하는 대신 NoSuchFieldException, NoSuchMethodException 을 반환합니다. 또한, getDeclearedFields, getDeclearedMethods 는 이러한 비 SDK 인터페이스가 제외된 결과만 반환합니다. 마찬가지로 Reflection을 제외하고도 JNI의 env->GetFieldID() , env->GetMethodID() 도 같은 결과를 발생시킵니다.

즉, 직접적인 Reflection 및 JNI를 통한 비 SDK 인터페이스의 사용이 모두 제한됩니다.

단, 모든 비 SDK 인터페이스가 사용 불가능한 것이 아닌 동작에 따라 제한을 만들었는데, 그 리스트는 다음과 같습니다.

  • 화이트리스트(모두 허용): SDK
  • 라이트 그레이리스트: 여전히 엑세스가 가능한 비 SDK 메서드 / 필드. 다만 다음 버전에서 이 리스트에 있는 것이 다크 그레이리스트 나 블랙리스트로 이동되지 않는다는 보장이 없습니다.
  • 다크 그레이리스트:

    • targetSDKVersion 가 28 미만일 경우 -> 다크 그레이리스트 인터페이스 사용 허용
    • targetSDKVersion 가 28 이상일 경우 -> 블랙리스트와 같은 동작
  • 블랙리스트: targetSDKVersion 상관 없이 제한되며, 상기된 NoSuchFieldExceptionNoSuchMethodException 의 제한이 적용됩니다. 즉, 플랫폼은 인터페이스가 없는 것 처럼 동작합니다.

여전히 라이트 그레이리스트에 있는 항목은 사용이 가능하지만, 상기된 내용과 같이 다음 버전에서도 작동한다는 보장이 안 되므로 변경할 수 있을 때 모두 변경하는 것이 좋습니다.

현재 앱이 비 SDK 인터페이스를 사용하고 있는지에 대한 판단 방법

StrictMode 클래스의 detectNonSdkApiUse 메서드를 사용하여 검출합니다.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    StrictMode.setVmPolicy(
            StrictMode.VmPolicy
                    .Builder()
                    .detectNonSdkApiUsage()
                    .build())
}

비 SDK 인터페이스 사용시 아래와 같은 로그가 출력됩니다.

Accessing hidden method Landroid/gesture/Gesture;->setID(J)V (blacklist, reflection)

StackTrace도 출력됩니다.

D/StrictMode(19092): StrictMode policy violation: android.os.strictmode.NonSdkApiUsedViolation: Landroid/widget/Toast;->mDuration:I
D/StrictMode(19092):    at android.os.StrictMode.lambda$static$1(StrictMode.java:428)
D/StrictMode(19092):    at android.os.-$$Lambda$StrictMode$lu9ekkHJ2HMz0jd3F8K8MnhenxQ.accept(Unknown Source:2)
D/StrictMode(19092):    at java.lang.Class.getDeclaredField(Native Method)
D/StrictMode(19092):    at com.os.operando.non_sdkinterfaces.sample.MainActivity$onCreate$3.onClick(MainActivity.kt:49)
D/StrictMode(19092):    at android.view.View.performClick(View.java:6597)
D/StrictMode(19092):    at android.view.View.performClickInternal(View.java:6574)
D/StrictMode(19092):    at android.view.View.access$3100(View.java:778)
D/StrictMode(19092):    at android.view.View$PerformClick.run(View.java:25883)
D/StrictMode(19092):    at android.os.Handler.handleCallback(Handler.java:873)
D/StrictMode(19092):    at android.os.Handler.dispatchMessage(Handler.java:99)
D/StrictMode(19092):    at android.os.Looper.loop(Looper.java:193)
D/StrictMode(19092):    at android.app.ActivityThread.main(ActivityThread.java:6642)
D/StrictMode(19092):    at java.lang.reflect.Method.invoke(Native Method)
D/StrictMode(19092):    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
D/StrictMode(19092):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

StrictMode 는 런타임 상에서의 검출이지만, apk를 대상으로 할 때에는 AOSP가 제공하는 veridex를 사용합니다.

veridex는 shell script 명령으로 실행시킬 수 있습니다. : ./appcompat.sh --dex-file=test.apk

부가적으로 OEM 업체 (삼성, LG) 가 자체 리스트를 추가할 수 있으나, 기본 리스트를 삭제할 수는 없으므로 기본이 되는 AOSP의 리스트에 대한 호환성은 모두 확보해야 합니다.

전원 관리 – 대기 버킷, 배터리 세이버 개선

앱 대기 버킷은 해당 앱이 얼마나 최근에, 자주 사용되는지에 따라 앱의 리소스 요청에 우선순위를 부여합니다. 총 5개의 버킷이 존재합니다.

Active

사용자가 앱을 현재 사용중이면 Active 버킷에 배치됩니다. 예를 들어 액티비티를 시작하거나 포그라운드 서비스를 실행중인 경우, 알림을 클릭한 경우에 해당됩니다.

Working Set

앱이 자주 실행되지만 현재는 활성이 아닌 경우에는 Working Set 버킷에 배치됩니다. 예를 들어,
소셜 미디어 앱은 Working Set에 있을 가능성이 높습니다. 또한 앱이 간접적으로 사용될 경우에도 Working Set
버킷으로 승격됩니다.

Frequent

앱이 매일 실행되는 것은 아니지만 정기적으로 사용되는 경우에는 Frequent 버킷에 배치됩니다.

Rare

앱이 자주 사용되지 않으면 Rare 버킷에 배치됩니다.

Nerver

설치되었지만 한 번도 실행되지 않은 앱은 Never 버킷에 배치됩니다.

시스템은 각 앱을 우선순위 버킷에 동적으로 할당하며, 필요한 경우에는 앱을 재할당합니다. 버킷은 앱이 얼마나 자주 실행되는지, 얼마나 자주 알람을 트리거하는지, 우선순위가 높은 Firebase Cloud Messaging 메세지를 자주 수신하는지에 대해 판별합니다. 이러한 제한은 기기가 배터리 전원을 사용 중인 동안에만 적용되며, 기기가 충전 중일 때는 시스템이 이러한 제한을 적용하지 않습니다.

부가적으로, Doze 허용 목록에 있는 앱은 대기 버킷의 영향을 받지 않습니다.

배터리 세이버 개선에 있어서는 여러 사항이 제한되었는데, 기기 제조업체가 적용되는 정확한 제한 사항을
결정합니다. 예를 들어, AOSP에서는 앱이 유휴 상태가 되기를 기다리는 대신 보다 적극적으로 대기 모드로 전환하며, 백그라운드
실행 제한을 앱의 targetSDKVersion 과 상관 없이 모두 적용하는 등의 행동을 결정합니다.

아래 표는 전원 관리에 대한 제한 표 입니다.

(1) : 작업 제한이 강제적일 경우, 특정 주기마다 10분동안의 작업이 허용됩니다. 10분이 지난 경우에는 모든 작업은 다음 대기열로 미뤄집니다.

(2) : 알람 제한이 강제적인 경우, 모든 알람은 스케쥴된 대기열에 발동되며, 10초동안 작업이 가능합니다.

(3): 네트워크 접근이 제한되었을 경우, 특정 주기마다 10분동안 네트워크를 사용하는 것이 가능합니다.

(4): 우선순위가 높은 FCM 메세지를 수신했을 때에 상한에 도달해있으면 그 이후의 메세지는 보통 우선순위를 가진 메세지로서 처리됩니다.

백그라운드에서의 센서 (마이크, 카메라, 가속도계, 자이로스코프) 엑세스 제한

앱이 마이크, 카메라, 가속도계, 자이로스코프 등의 센서를 사용중인 경우 백그라운드 상태에서는 이 센서의 정보를 받지 못합니다. 앱이 센서 이벤트를 감지해야 하는 경우, 포그라운드 서비스를 사용해야 됩니다.

통화 로그 엑세스 제한

기존에 존재하던 READ_CALL_LOG, WRITE_CALL_LOGPROCESS_OUTGOING_CALLS 권한이 위치하던 PHONE 그룹에서 신설되는 CALL_LOG 그룹으로 이동됩니다. 이 CALL_LOG 그룹은 전화 통화 기록을 보고 전화번호를 식별하는 등과 같이 전화 통화와 관련된 민감한 정보를 엑세스하는 데에 필요한 기능을 제공합니다.

앱에서 통화 로그에 엑세스 해야 하거나 발신 통화를 처리해야 할 경우, Runtime
Permissions 요청을 통해 이 권한을 적절히 요청해야 합니다. 이러한 권한은 사용자가 앱에서 전화 통화 기록 정보에
엑세스하지 못하도록 거부할 수 있으므로, 정보에 엑세스하지 못하더라도 이를 적절히 처리할 수 있어야 합니다.

전화번호 엑세스 제한

READ_CALL_LOG 권한을 취득하지 않는 이상, 전화 상태 브로드캐스트 및 PhoneStateListener 클래스에서 전화번호 필드가 비어있게 됩니다. PHONE_STATE 브로드캐스트에서 전화번호를 읽으려면 READ_CALL_LOG 권한과 READ_PHONE_STATE 권한이 모두 필요합니다. 또, onCallStateChanged() 에서 전화번호를 읽으려면 READ_CALL_LOG 권한만 필요로 합니다.

부가 사항들

  • 앱이 Wi-Fi를 스캔하는 데에 필요한 권한 요구사항이 엄격해졌습니다. 정리하면, WifiManager.startScan()ACCESS_FINE_LOCATION 또는 ACCESS_COARSE_LOCATION 권한을 가지고 있고, CHANGE_WIFI_STATE 권한을 가지고 있어야 합니다. WifiManager.getScanResult()ACCESS_FINE_LOCATION 또는 ACCESS_COARSE_LOCATION 권한을 가지고 있고, ACCESS_WIFI_STATE 권한을 가지고 있어야 합니다.
  • UTC는 더 이상 GMT와 동의어가 아닙니다.
  • Android 9의 UTF-8 디코더는 이전 버전보다 더욱 엄격하게 Unicode 표준을 준수합니다.
  • /proc/net/xt_qtaguid 폴더에 있는 파일을 직접 읽을 수 없게 됩니다. 이는 이러한 파일이 전혀 없는 기기와의 일관성을 유지하기 위해서 입니다.
  • Android 9부터 Crypto JCA 제공자가 제거되었습니다. SecureRandom.getInstance("SHA1PRNG", "Crypto")를 호출하면 NoSuchProviderException이 발생합니다.

Andorid Pie를 대상으로 하는 앱에 적용되는 지원 사항

  • 포그라운드 서비스를 생성하기 위해서는 FOREGROUND_SERVICE 권한을 요청해야 합니다. 이 권한은 정상 권한이므로 자동으로 권한을 부여합니다. 이 권한 없이 포그라운드 서비스를 시작하면 SecurityException 가 발생합니다.
  • Android 9에서는 Bouncy Castle 제공자가 제공하는 여러 암호화가 지원 중단되고 Conscrypt 제공자가 제공하는 암호화가 대신 사용됩니다. Bouncy Castle 제공자를 요청하는 getInstance() 호출은 NoSuchAlgorithmException 오류를 생성합니다. 이 오류를 해결하려면 getInstance()에서 제공자를 지정하지 마십시오(즉, 기본 구현 요청).
  • Build.SERIAL 가 UNKNOWN 으로 고정됩니다. 단, 하드웨어 일련번호에 엑세스해야 하는 경우에는 READ_PHONE_STATE 권한을 요청한 다음, getSerial() 을 호출해야 합니다.
  • 앱이 더 이상 여러 프로세스에서 단일 WebView 데이터 디렉토리를 공유할 수 없습니다. 앱에 android.webkit
    패키지의 WebView, CookieManager 또는 기타 API를 사용하는 프로세스가 2개 이상 있는 경우, 두 번째
    프로세스에서 WebView 메서드를 호출하면 앱이 중단됩니다.
  • 시스템은 각 앱의 비공개 데이터 디렉토리 에 대한 앱별 SELinux 샌드박스를 적용합니다. 이제 경로를 통해 다른 앱의
    데이터 디렉토리에 직접 엑세스하는 것은 허용되지 않습니다. 앱이 FD 전달을 포함한 IPC 메커니즘을 사용하여 데이터를 계속
    공유할 수도 있습니다.
  • 0 영역 (너비나 높이가 0) 이 있는 뷰는 포커스가 불가능합니다. 또한 터치모드에서는 액티비티가 더 이상 초기 포커스를 암시적으로 할당하지 않습니다. 그 대신, 원할 경우 초기 포커스를 명시적으로 요청해야 합니다.

reuse DatagramSocket in UDP (already use)

UDP 로 패킷을 받아 작업을 진행하는 기능이 있었다. 이 때 사용하는 클래스는 DatagramSocket 인데, 가끔 already use 라는 로그가 뜨면서 앱이 종료되는 일이 있다. close 하면 괜찮다고 생각했지만, 어느 시점에서 계속 발생하는 것 같았다.

결국에는, close() 말고도 다른 작업을 했어야 하는데, 좀 더 찾아보니 DatagramSocket 에 setReuseAddress 라는 메서드가 있어 이 것을 설정해주면 문제는 해결된다는 것이다. 문제는, DatagramSocket 의 바인딩 시점은 ‘생성자’ 에서 한다는 것이다. 즉, setReuseAddress 를 사용하기 위해서는 DatagramSocket 의 인스턴스가 필요한데 이 시점에서 바인드가 되니 설정할 수 없는 것이다.

즉, setReuseAddress 를 사용하기 위해서는 바인딩 시점을 피해 인스턴스를 생성하는 것인데, 결론으로 말하자면 DatagramSocket 의 생성자 파라미터에 null를 넘기는 것이다.

이렇게 하면 문제가 해결될 것 같지만, 한 가지 더 문제가 발생했다. DatagramSocket 의 인스턴스를 생성하고 bind 메서드를 부르려 하니 bind 메서드는 InetAddress 클래스만 받고 있었다. 필요한 기능은 루프백 IP + 특정 Port 조합이므로 원래라면 DatagramSocket(port) 만 넘기고 있었으나, bind 메서드를 사용하기 위해서는 InetAddress 객체를 만들 필요가 있었다.

일반적으로 루프백 IP 라 함은 127.0.0.0 ~ 127.255.255.255 를 의미하니 InetSocketAddress(“127.0.0.1”, port) 를 넘기면 되겠다 싶었지만 작동이 안 되었다.

결국엔 DatagramSocket 의 자바독 문서를 보고 답을 찾았는데, ‘If the IP address is 0.0.0.0, the socket will be bound to the wildcard address, an IP address chosen by the kernel. ‘ 라는 문구이다.

이 문구는 DatagramSocket 의 수 많은 생성자들 중 두 개에 있었는데, 먼저 DatagramSocket(port, InetAddress) 의 설명을 보면 아래와 같이 작성되어있다.

Creates a datagram socket, bound to the specified local address. The local port must be between 0 and 65535 inclusive. If the IP address is 0.0.0.0, the socket will be bound to the wildcard address, an IP address chosen by the kernel.

Datagram socket 를 구성하고 특정 로컬 주소로 바인드 합니다. 포트는 0 과 65535 내에 있어야 합니다. IP Address 가 0.0.0.0 이면, 커널에 의해 IP address 가 결정되는 와일드카드 주소로 바인드 됩니다.

그리고 원래 사용하던 DatagramSocket(port) 의 설명에도 비슷한 말이 작성되어 있었다.

Constructs a datagram socket and binds it to the specified port on the local host machine. The socket will be bound to the wildcard address, an IP address chosen by the kernel.

Datagram socket 를 구성하고 로컬 호스트 머신에 있는 특정 포트에 바인드 합니다. 이 소켓은 커널에 의해 IP address 가 결정되는 와일드카드 주소로 바인드 됩니다.

즉, InetSocketAddress(“127.0.0.1”, port) 가 아닌 InetSocketAddress(“0.0.0.0”, port) 를 넘기면 원래 사용하던 DatagramSocket(port) 와 같은 효과를 낼 수 있다는 것이다.

이렇게까지 해서 완성된 메서드는 다음과 같다.

/**
 * original DatagramSocket(port) will be bound to the wildcard address, an IP address chosen by the kernel.
 * So, we can bound to the wildcard address using 0.0.0.0 into hostname
 *
 * See original document [https://docs.oracle.com/javase/6/docs/api/java/net/DatagramSocket.html#DatagramSocket(int,%20java.net.InetAddress)]
 *
 * @param port Int
 * @return Observable<DatagramSocket>
 * @see DatagramSocket
 */
fun createReuseSocket(port: Int): Observable<DatagramSocket> {
    return Observable.create<DatagramSocket> {
        val socket = DatagramSocket(null)
        socket.reuseAddress = true
        socket.bind(InetSocketAddress("0.0.0.0", port))
        it.onNext(socket)
    }
}

대표 이미지는 http://ithare.com/almost-zero-additional-latency-udp-over-tcp/ 로부터 가져왔습니다.

Load my friends of Facebook with RxJava

도입

SNS를 연동하는 앱을 개발하다 보면, 가끔 ‘친구 목록’ 을 불러와서 처리하는 기획이 있다.

Facebook SDK 내부의 GraphRequest 클래스 내부에는 여러 메서드가 있는데, 기본적으로 로그인을 했을 때 나 자신의 정보를 불러오는 newMeRequest 메서드나 글을 올리는 newPostRequest 메서드도 있다.

여기에는 나 자신의 친구 목록을 불러오는 newMyFriendsRequest 메서드도 있는데, 이 메서드를 이용하여 나 자신의 친구 목록을 불러오는 기능을 만들 수 있다.

이 글에서는 newMyFriendsRequest 사용과 함께 Async Callback 를 RxJava 의 Observable로 발행하는 방법을 다뤄보려 한다.

기본 코드

GraphRequest.newMyFriendsRequest 의 파라미터는 총 2개인데, 첫 번째 파라미터인 accessToken 은 Facebook API에 접근하기 위한 엑세스 토큰이고, 두 번째 파라미터인 callback 는 GraphJSONArrayCallback 타입으로 JSONArray 형태로 콜백을 받기 위한 인터페이스이다. 즉, Request 로 요청하면 그 결과가 GraphJSONArrayCallback.onCompleted 에 오는 것이다.

따라서 기본적으로 해줘야 할 것은 GraphJSONArrayCallback 에 대한 지역변수를 만들고, 이를 이용해 Request 클래스를 만들어 Asynchronous 방식으로 요청하는 것이다.

그에 대한 기본 코드는 다음과 같다.

private void getFriendsInfo() {
    GraphRequest.GraphJSONArrayCallback callback1 = (array, response) -> {
        
    };

    GraphRequest request = GraphRequest.newMyFriendsRequest(AccessToken.getCurrentAccessToken(), callback1);
    Bundle parameters = new Bundle();
    parameters.putString("fields", "id, name, email, gender, birthday, picture, first_name");
    request.setParameters(parameters);
    request.executeAsync();
}

여기에 간단한 콜백 방식을 붙이면 다음과 같다.

private void getFriendsInfo(F1<Boolean> listener) {
    GraphRequest.GraphJSONArrayCallback callback1 = (array, response) -> {
        if (array == null) {
            listener.invoke(false);
            return;
        }

        listener.invoke(true);
    };

    GraphRequest request = GraphRequest.newMyFriendsRequest(AccessToken.getCurrentAccessToken(), callback1);
    Bundle parameters = new Bundle();
    parameters.putString("fields", "id, name, email, gender, birthday, picture, first_name");
    request.setParameters(parameters);
    request.executeAsync();
}

이렇게 사용해도 별 문제가 없지만, RxJava 를 도입함으로서 Callback hell 를 최소화시키고 새로 구현한 RxSocialLogin 와도 잘 통합하기 위하여 Async Callback 방식을 RxJava 의 Observable 로 바꾸려 한다.

RxJava 의 도입

RxJava 도입을 위해서는 기존의 콜백 방식에서 Observable 로의 전환이 필요한데, Observable 는 비동기 데이터 스트림 (Asynchronous Data Stream) 에 기반한 방식으로 동기/비동기 방식을 별개로 처리하는 기존 방식 보다는 연속적인 흐르는 데이터 스트림으로 동기/비동기 방식을 처리하는 것이다.

따라서 Callback 를 Observable 로 변환해야 하는데, 여기에서 가장 흔히 사용되는 것은 Observable.create 메서드이다. 이 메서드는 ObservableOnSubscribe 라는 인터페이스를 파라미터로 받는데, 이 인터페이스는 ObservableEmitter 인터페이스를 파라미터로 가지고 있어, onNext, onError, onComplete 를 발행할 수 있게 해준다.

onNext, onError, onComplete 는 그 이름 대로 역할이 있는데, onNext 는 스트림에 데이터를 발행하기 위해, onError 는 스트림에 에러를 발행하고 스트림을 종료하기 위해, onComplete 는 모든 작업이 끝나 스트림을 종료하려 할 때 사용한다. onError 는 onNext 와 달리 onComplete 를 명시적으로 호출할 필요가 없다.

Observable.create 는 Observable<T> 라는 클래스를 반환하는데, 이 Observable 를 가지고 리액티브 연산자를 활용하여 최종적으로 Subscribe 를 하면 Subscribe 한 곳에 데이터가 들어오는 형식이다.

예를 들어, 이런 방식이다.

// due to limitation, divide chain into two group
// see https://github.com/WindSekirun/RxSocialLogin#limitations
Disposable disposable = RxSocialLogin.facebook(facebookLogin)
        .subscribeOn(AndroidSchedulers.mainThread())
        .subscribe(resultItem -> FacebookUtils.newMyFriendsRequest()
                .flatMap(data -> mSettingRepository.updateMemberFriendList(mActionMemNo, encodeList(data)))
                .subscribe(data -> loadData(), EMPTY_ERROR), EMPTY_ERROR);

addDisposable(disposable);

이제 위 기본 코드를 RxJava 방식으로 변경하면 다음과 같다.

public static Observable<List<Boolean>> newMyFriendsRequest() {
    return Observable.create(emitter -> {
        GraphRequest.GraphJSONArrayCallback callback1 = (array, response) -> {
            if (array == null) {
                emitter.onError(new Exception("no friends list"));
                emitter.onComplete();
                return;
            }

            emitter.onNext(true);
            emitter.onComplete();
        };

        GraphRequest request = GraphRequest.newMyFriendsRequest(AccessToken.getCurrentAccessToken(), callback1);
        Bundle parameters = new Bundle();
        parameters.putString("fields", "id, name, email, gender, birthday, picture, first_name");
        request.setParameters(parameters);
        request.executeAsync();
    });
}

마무리

이전부터 리액티브 프로그래밍이 좋다고 계속 들어왔지만, 쉽게 도입할 생각이 안 들었기도 했다. 하지만 이번 프로젝트를 진행하면서 RxJava 를 적극적으로 도입하니, 겁 먹을 필요가 없다고 느껴졌다. 기존에 Stream 을 적극적으로 사용해서 익숙해진 것일지도 모르겠다.

이제 나름대로 MVVM + DataBinding + Dagger + Kotlin + RxJava stack 를 가지게 된 셈인데, 좀 더 공부해서 함수형 프로그래밍 까지 잘 활용할 수 있게 노력해야 겠다고 생각했다.