RxJava 1 과 비교해서 정리한 RxJava 2

RxJava 1 적용한지 얼마 안 되었는데, 벌써 RxJava 2 라니, 말도 안 돼! 심지어 3.X 에 대한 정보도 심심찮게 들리는데…

서론

RxJava1과 RxJava2 를 먼저 비교하기에 앞서, 왜 이 진입 장벽 높은 라이브러리가 버전업을 했는지부터 알아야 할 필요가 있다. Reactive Extension (Rx)은 옵져버 패턴을 이용해서 이벤트 기반 비동기식 프로그래밍을 할 수 있게 만들어준 라이브러리다. 다양한 언어에서 이 Rx의 프로토콜을 이용할 수 있게 기준이 되는 것이 Reactive Stream Specification인데, RxJava 1은 이 기준에 맞지 않는 것들이 많아서, 버전업을 시키면서 완전히 처음부터 다시 작성하기로 결정된 것이다.

다른점

RxJava 2 에서 변경된 것들은 많지만, 일단 대분류로 그 중 인상적인 것들만 모아봤다. 이들을 제외하고도 달라진 점들은 꽤 많으니, 직접 진행하는 프로젝트에 일단 넣어보고 고민하는 것도 괜찮은 방법이다. 목록이 긴 관계로 관심 있는 대분류를 클릭해서 보면 편할 것이다.
Observable.create
Null
Functional Interfaces
Testing
Observable vs Flowable
Single, Completable and Maybe

결론

RxJava1 을 사용하면서 이미 Backpressure 에 대한 이해가 있었다면, 빠른 시일 내에 업데이트하는 게 좋을 것 같다. Dagger 1 에서 Dagger 2로 넘어가는 것만큼 사고의 변환이 크지 않고, Technical Debt 기간이 길어질수록 이자가 쌓여서, 어차피 나중에 전환할 때 더 힘들어지기 때문이다. 이 블로그와 같이 90% 이상이 RxJava 1 베이스인 경우라면, PR을 여러 기능별로 끊어서 넣고 배포 때 플레이스토어의 Staged Rollout 기능을 통해서 예상치 못하는 이슈들을 잡아나가면서 진행하는 걸 추천한다.

Observable.create()

RxJava 1의 가장 큰 진입 장벽의 하나던 .create() 함수가 전면적으로 개편되었다. API를 처음 배울 때 가장 처음 확인하게 되는 .create() 함수를 올바르게 쓰기가 너무 어려운 관계로 .just().defer() 을 함께 많이 이용했는데, RxJava 2에서부터는 Flowable 의 경우 .create() 의 경우 BackpressureStrategy 를 미리 지정해줘야 하게 된다. Back pressure 을 책임져야 하는 존재는 source이어야 하며, chain이 아니라는 철학이 반영되었다.

Null

RxJava 1 에서 자주 보이던 Observable.just(null), 특히 RxBinding 을 같이 쓰는 프로젝트에서 아주 자주 보게 되는데, RxJava 2 에서는 더는 null을 정상적인 값으로 받거나 넘기지 않게 된다.

위와 같이 null을 값으로 받거나 넘기게 될 경우, NullPointerException 으로 취급해서 에러로 핸들 하게 된다. Observable 의 경우 오로지 완료되거나 오류를 던지는 것 외에는 값을 넘길 수 없게 되며, API 제작자들은 enum 타입으로 분리하던 객체를 이용해서 Observable 처럼 내려줄 객체를 정의해야 한다.

Functional Interfaces

RxJava 1.X 와 2.X 모두 Java 6의 호환을 고려하고 설계되어서 Java 8의 java.util.function.Function 에서 제공하는 Functional Interface 들을 사용하지 못해서, RxJava 내부에서 별도로 정의했었다. 단지, 이제 RxJava 2.X부터는 Exception을 핸들 하게 되어서, 더는 .map() 안에서 try-catch 코드를 보지 않아도 된다! 예를 들면, 아래와 같다:

Testing

RxJava 1.X의 TestSubject 가 드디어 없어졌다! TestSchedulerPublishSubject 를 통해서 손쉽게 테스팅을 할 수 있게 되었다. 개인적으로는 유닛 테스트 작성할 때 PublishSubjectRxJavaPlugins로 구현했었는데, 이제 injection을 통해서 스케쥴러를 주입할 수 있게 짜는 방향으로 가게 될 것 같다.

Observable vs Flowable

상용 프로젝트 환경에서 Rx 스트림 내에서 MissingBackpressureException 혹은 OutOfMemoryError 을 겪게 되면 매우 대응하기가 난감하다. 특히 RxJava 1 에서는 백프레셔를 대응할 수 있는 오퍼레이터인지 아닌지, 그리고 어떻게 대응을 할지에 대해서 알기 위해선 일단 한번 당하고(?) 문서를 확인하며 배우는 방식이다. 근본적으로 Hot Observable은 올바르게 백프레셔를 처리하기 힘들며, 애초에 RxJava 1.X에서는 Hot Observable과 Cold Observable으로 명확하게 나뉘어있지 않기 때문이다.

RxJava 2.X에서는 이 둘을 명확하게 나누기 위해서 백프레셔가 일어나지 않는 Observable과 백프레셔를 주의해야 하는 Flowable로 나누게 되었다. 아래는 RxJava 위키에서 정의하는 Observable과 Flowable 을 사용해야 할 때를 정리한 것이다:

Observable을 사용 해야 할 때

  1. 흐르는 객체에 따라 다르겠지만, OOM이 일어날 수 없을 정도의 객체의 개수를 관리할 때
  2. 터치이벤트나 클릭 좌표 이동 등 sampling 또는 debouncing으로 쉽게 핸들 할 수 있는 GUI 이벤트 사용할 때
  3. 이때 항상 유의해야 하는 것은, Observable이 Flowable 보다 적은 자원을 필요로 하다는 점이다

Flowable 을 사용 해야 할 때

  1. 수많은 객체들이 생성되며 흘러내려 올 때 chain이 생성하는 소스의 수를 제한할 수 있을 때
  2. 디스크에서 읽기 혹은 파싱 해서 객체를 내려줄 때
  3. JDBC를 통해서 데이터베이스에서 읽고 내려줄 때
  4. 네트워크 IO를 통해서 값을 내려줄 때

Single, Completable and Maybe

Single

싱글 타입은 하나의 onSuccess 혹은 onError 만을 내려줄 수 있는 타입이다. 소비자 역할이 되는 SingleObserver은 3개의 메소드만 가지고있는 인터페이스이며 API는 아래와 같다:

Completable

Completable 타입은 onComplete 혹은 onError 만을 내려줄 수 있는 타입이다. 소비자 역할이 되는 CompletableObserver은 3개의 메소드만 가지고 있으며 API 는 아래와 같다:

Maybe

RxJava 2.0.0-RC2 에서 Maybe 라는 새로운 베이스 타입을 소개했다. 이론적으로는 위의 Single 과 Completable 을 합친것과 같다. API 는 아래와 같다:

결론

결론다시보기

Reference

  • https://msdn.microsoft.com/en-us/library/hh242985(v=vs.103).aspx
  • http://www.reactive-streams.org/
  • https://github.com/ReactiveX/RxJava/wiki/What’s-different-in-2.0
  • https://artemzin.com/blog/reply-to-kaushik-gopals-aricle-rxjava-1-rxjava-2-understanding-the-changes/
  • http://blog.kaush.co/2017/06/21/rxjava1-rxjava2-migration-understanding-changes/
  • https://blog.mindorks.com/migrating-from-rxjava1-to-rxjava2-5dac0a94b4aa

RxRealcase 2장: 네트워크가 불안정한 존을 위한 Exponential Backoff

About

RxRealcase 시리즈는 가상의 개발자와 가상의 앱을 이용해서 RxJava Operator 들의 실제 적용 사례를 정리한 글이다. 가볍게 읽을 수 있으며, 필요할때 적용 할 수 있는 의미로 작성하고 있다. 이번 장은 RxJava를 적용하며 불안정한 네트워크에서 비교적 우아하게 서버에 재호출을 하는 방법에 대해서 고민을 하는 글이다. https://github.com/wotomas/Findeed 에서 전체 코드가 언젠가 공개될 예정이다. #experienceMatters

시나리오

이상

존은 성공한 사업가로서 필수적으로 설치를 해야한다는 Findeed 라는 앱을 자주 사용한다. 사업차 해외로 출장을 자주 가는 존은 간혹 네트워크가 불안정한 나라에서도 Findeed 를 아무 문제없이 사용한다. 네트워크가 불안정한 곳에서 접속 할때에는 존은 향긋한 헤이즐넛 라떼를 마시며 기분 좋게 기다린다.

하지만…

현실

존은 가끔 출장 중에 Findeed 라는 앱을 사용하지만 네트워크가 불안정한 곳에서는 실행이 정상적으로 안되는 앱을 보면서 한숨만 푹푹 쉰다. 투자자의 연락을 확인해야하는 존은 수동으로 재시도를 몇번이나 시도한 끝에 겨우 접속에 성공하였다. 답변하기 버튼을 클릭하는 순간 네트워크가 불안정하다며 앱이 종료되어버린다.

해결

개발도상국에서 Findeed 의 접속자가 많아 지기 시작하자 앱 개발자는 네트워크 에러 핸들링을 개편하기로 결정한다. 기존에 실패시에 1초마다 3번 재시도를 하는 로직을 수정하기로 결정 한 것이다. 재시도 횟수에 따라 호출의 간격이 길어지면 좋겠다고 생각하며 개발자는 다시 한번 Rx의 심해로 빠져보기로 한다.

1. repeatWhen()

개발자는 repeatWhen() 을 사용하니 네트워크 호출이 정상적으로 돌아와도 다시한번 호출을 하게 되는것을 확인하였다. 네트워크 에러가 생길시에만 재호출을 하고 싶지만, repeatWhen()onComplete()이 호출될때 다시 Subscription 이 생성되는걸 확인했다. 이번 구현에는 부적합하다고 판단한 개발자는 다른 걸 찾아 보기로 했다.

2. retryWhen() + timer()

조금만 더 찾아보다 보니 개발자는retryWhen()을 발견했다! onError() 이 발생할때 다시 Subscription 을 생성할 수 있게 되니 개발자는 써보기로 결정했다. timer() 과 합쳐서 구현하면 간단한 재시도는 구현할 수 있겠다고 판단했다. 하지만 더 우아한 해결책을 원한 개발자는 더 찾아 보기로 결정한다.

3. retryWhen() + zipWith() + range() + timer()

개발자는 여러개의 operator들을 묶으면 구현 할 수 있겠다고 판단하였다. retryWhen()range()zipWith() 시켜서 재시도 횟수를 구현하고, timer() 을 이용해서 인터벌이 늘어 날 수록 다음 시도 간격을 증가 시키는 방법으로 결정하였다.

구현

코드가 조금 긴 관계로 Gist 를 확인하면 완성된 코드를 확인 할 수 있다.

Unit Test

RetryWithExpoentialBackOff 의 경우 testSubscriber 을 이용하여 다양한 상황을 테스트 한다.

Reference

  • 글에서 사용된 Findeed 라는 앱은 아직까지 공개 / 개발 되지 않은 가상의 앱이다.
  • https://en.wikipedia.org/wiki/Exponential_backoff
  • http://blog.danlew.net/2016/01/25/rxjavas-repeatwhen-and-retrywhen-explained/
  • https://gist.github.com/sddamico/c45d7cdabc41e663bea1
  • https://stackoverflow.com/questions/22066481/rxjava-can-i-use-retry-but-with-delay/25292833#25292833
  • http://leandrofavarin.com/exponential-backoff-rxjava-operator-with-jitter

Android Testing 정리

안드로이드의 테스팅의 역사는 짧고 역동적이다. 수많은 라이브러리나 툴들이 개발되었으며 (MonkeyRunner, UiAutomator, Espresso, Robotium, Calabash, Appium, Android Testing Framework, Spoon, Robolectric, etc) 그 짧은 시간 안에 이미 역사 속으로 사라진 툴들도 수없이 많다. 진입장벽이 높아 자연스레 개발자들이 “시간이 남으면 하는 것”이라는 인식이 강하지만, 알고 볼수록 더 즐거운 테스팅에 대해서 배워보자.

개념

Testing은 여러 종류로 나뉘며, 심지어 이름도 부르는 사람에 따라 다양하다. 명칭을 외우기 보다는 왜 Testing 이 여러 종류로 나뉘었는지 이해하면 Test Code 작성이 쉬워질 것이다.

이 글에서는 Unit Tests, Integration Tests 그리고 Functional Tests 에 대해서 이야기 할 것이다.

아래에서 더 자세하게 설명하겠지만, 일단 이 3종류의 Test를 정의하고 시작하겠다.

  1. Unit Test: JVM 상에서 구동되는 로컬 Test. 실행 시간이 매우 짧으며, 안드로이드 혹은 다른 라이브러리 코드에 의존성이 없어야 한다. OOP 언어에서는 가장 작은 단위가 interface 인 경우가 많다. 즉, 특정 클래스가 가지고있는 모든 public method가 테스트 단위가 되는 경우가 많다.

  2. Integration Tests: 단위 테스트가 완료된 모듈들을 합쳐서 하는 테스트를 Integration Test라고 칭한다. 이 시점에는 비즈니스 로직을 테스트하는 경우가 많은데, 이곳에서 비즈니스 로직은 돈을 버는 수단이 아니라, 앱에서 데이터가 가공되는 로직을 칭한다.

흔한 Integration Test 시나리오:
ApiManager.class가 데이터를 서버에서 받아서 UserController.class에 넘겨서 데이터를 업데이트 하고 UserInfoPresenter.class가 보여주는것을 관리하고 최종적으로 Fragment에 넘긴다.

Business Logic Flow

ApiManager, UserController, UserInfoPresenter 모두 인터페이스로 Unit Test가 가능하기 때문에 최종적으로 모든것이 연결되어서 Integration Test를 진행할 수 있게 되는것이다.

Integration Test 시점에서는 주로 UI Test를 많이 사용하는데, 이 시점에서는 Mock Server과 Mock Data를 사용해야한다. 왜 그러한지는 아래에서 Functional Test 부분에서 더 설명 할 것이다.

  1. Functional Tests: 기능에 관해서 하는 테스트이다. 사용성 테스팅, 성능 테스팅, 한계 테스팅, 보안 테스팅, 등 실제 사용자가 사용하는 환경과 똑같은 조건에서 진행되는 테스트는 다 이곳으로 들어간다.

실제 서버를 이용한 UI Test 는 이곳에 들어가게된다. 이 시점에서 사용되는 UI Test 는 다른 테스팅과 합쳐져서 사용되는 경우가 많은데, 성능 테스팅과 한계 테스팅이 자주 같이 사용된다. 한계 테스팅 (Stress Testing) 은 MonekyTest 등으로 자주 사용되는데, UI 이벤트를 쏘면서 앱에 행동을 자동으로 리포트화 시킨다. 성능 테스팅에 자주 사용되는 접근법은 빌드별로 FPS Profiling을 포함시켜서 리포트화 시키는 것이다.

테스팅의 피라미드

이런 여러 종류의 Testing을 어떻게 최적화해서 배치하는 것이 좋을까. Test의 안정성과 실제 사용 경험과 유사성은 반비례 관계이지만, 그렇다고 모든 Test를 Functional Test로 작성 한다면, 과도한 False Alarm에 테스트로서 가치가 떨어질 것이다.

고로, 최적화된 Testing Suite를 유지하기 위해선 아래의 규칙을 따르는 것을 권장한다:
70-80% Test는 Unit Test로 이뤄 코드의 안정성을 테스트한다
20-30% Test는 Integration Test로 이뤄 앱의 실제 구동 환경을 테스트한다
10-20% Test는 Functional Test로 이뤄 사용자 경험을 최대화시킬 수 있는 요소들을 테스트한다

이것들을 숙지하고 이제 각 Testing의 구체적인 정보들을 익혀보자.

Always write a test to reproduce a bug before you fix it – Robert C. Martin

Unit Test

안드로이드에서의 Unit Test 는 JUnit 4 로 이뤄져있기를 권장한다. JUnit은 자바에서 가장 널리 사용되는 유닛 테스팅 프레임워크며, 버젼 3과 4의 차이는 성능적인 면에서도 어마어마하며, 불필요한 양식을 따를 필요가 없기때문이다. JUnit 3 에서는 테스트 클래스가 junit.framework.TestCase 클래스의 자식으로 생성되었어야하지만, JUnit4 부터는 그럴필요가 없어졌다. (고로 TestCase class가 사용되는 예제를 보면 Maintain 이 되지 않은 프로젝트임을 인지하고 읽는것을 추천한다) JUnit 4 에서 부터는 @Test annotation 을 이용해서 테스트 케이스를 작성한다.

Unit test는 코드의 가장 작은 단위의 코드를 테스트하는 코드이다. 이 단위는 메소드 혹은 클래스가 되는 경우가 대부분이며, 특정 클래스/메소드가 예상한것처럼 구동하는지를 테스트 하는 것이다. 다시 말해, 특정 클래스의 모든 Public 메소드가 구현한대로 작동하는지, 그리고 특정 클래스의 state가 구현한대로 올바른 state에 존재하는지, 등을 테스트 하게 된다.

런타임에 Unit Test는 모든 final 한정자가 삭제된 android.jar 파일에 대해서 test가 실행되며, Unit Test에 사용되는 jar 파일에는 실제 로직이 들어간 코드가 전혀 없으며, 호출될시 exception을 던지게 되어있다 (즉, android.text.TextUtil.isEmpty() 등 이 존재하는 로직이 test에서 깨지는 이유).

테스트를 하는 클래스나 메소드의 dependencies들을 Mockito 라는 라이브러리로 mock을 해서 unit과 dependencies 들을 분리 시킬 수 있는 것이다. 테스트 코드 내부에는 테스트 하는 Unit만 Concrete class로 구현하고, 그 외에 클래스들은 dummy, stub, mock, spy, fake 등을 이용해서 구현하는 것이 좋다.

그 외에, 가독성을 위해 한 테스트 케이스 안에서는 하나의 assertion (mockito를 사용한다면 verify) 이 존재하는것을 권장하며, 모든 테스트는 트리플A (Arrange, Act, Assert) 의 원칙을 지켜야한다. (세팅을 하고 → 실행을 시키고 → 확인을 한다)
그렇다면 언제 Unit Test를 짜야하나? 유닛 테스트는 항상 두려움에 의해서 작성되어야한다. 두려움이 없으신 분이라면, 안전하게 모든 public 메소드를 테스트 하는것을 목표로 잡는것도 무방하다.

Integration Test

Functional Test는 하드웨어나 에뮬레이터를 통해서 구동되는 테스트를 칭한다. 안드로이드의 Instrumentation API / UIAutomator 를 통해서 구동되며, 대표적인 라이브러리로는 Espresso, Robotium, Appium 등이 있다. Context에 대한 접근이 가능하며, 사용자들의 행동 또한 테스트 할 수 있다. 테스트 중에 서버의 값을 받아서 화면에 정상적으로 출력이 되는지 확인 하기 위해서, 서버에서 돌려받는 값을 mock해서 테스트를 바로 할 수 있는 것이다. Instrumented Test를 통해서 만들어진 별도의 APK로 테스트가 만들어지만, 실제 앱 APK에는 영향을 주지 않는다.

Functional Test의 경우 많은 라이브러리들이 존재하며, 라이브러리들 마다 개별적인 버그가 존재한다. 구글이 espresso 를 공식 지원하기로 한 이상, 메인 스트림은 espresso이 될 가능성이 가장 크다. CI와 연동시킨다면 서버에서 에뮬레이터를 통해서 UI 자동화 테스트가 돌아가게 만들 수 있기때문에, Unit Test와 함꼐 자동화 툴과 연동이 가능하다.

Functional Test

Functional Test의 공식적인 뜻은 소프트웨어에서부터 하드웨어까지 모든 컴포넌트간의 상호작용을 테스트 하는것을 말한다. 이 글에서 Functional Test 는 추상적이고 포괄적인 개념으로 사용했으며, 대표적인 예로는 Performance, Stress, Usability Test 등이 있다.

Performance Test (Traceview, profiling, dumpsys etc..)

Performance Test는 앱의 성능을 테스트 하는 기능으로, traceview 혹은 profiling 을 통해서 앱의 성능을 수치화해서 확인을 하는 방식이다. 주로 Cold start time, 평균 FPS, 메모리 / CPU 소모량 등을 테스트를 통해서 수치화 시켜서 리포트화 시키는 경우가 많다. Functional Test 경우 주로 많이 성숙해진 프로젝트에 사용된다. 왜냐하면:

premature optimization is the root of all evil

Stress Test (Moneky Test)

안드로이드에서 stress test는 monkey test를 주로 사용하며, 마치 원숭이에게 핸드폰을 던져줬을때의 상황을 묘사한다고 해서 지어진 이름이다. 핸드폰에 무작위의 수많은 사용자 이벤트들을 발생시키며 앱이 터지는 경우 그 케이스를 개발자들에게 전달 해주는 방식으로 이뤄져있다.

결론

테스팅에는 명확한 규칙은 없다. 하지만 장려하는 분포도는 70 | 20 | 10이다. 가장 중요 한것은 개발 팀원들이 다같이 테스트에 대한 자주적 최적화, 그리고 코드 품질과 제품 개발 비용 최소화에 대한 고민을 지속적으로 하며 유기적으로 변해가는 테스팅 문화를 만들어가는것이 중요 할 것이다.

I am an ordinary developer. If this blog post contains any wrong information or improvements, please feel free to add comments! I would love to hear it, to improve and learn together. 😀

Reference:

https://developer.android.com/studio/test/index.html
https://developer.android.com/training/testing/index.html
https://developer.android.com/training/testing/start/index.html
http://www.vogella.com/tutorials/AndroidTesting/article.html
https://www.tutorialspoint.com/android/android_testing.htm
http://www.guru99.com/why-android-testing.html
https://www.toptal.com/android/testing-like-a-true-green-droid
https://codelabs.developers.google.com/codelabs/android-testing/index.html?index=..%2F..%2Findex
https://github.com/hotchemi/awesome-android-testing
http://blog.danlew.net/2014/01/23/testing-on-android-part-3-other-testing-tools/
https://docs.google.com/spreadsheets/d/1IPvDArF5o7pNZ81SYealqVNc5Pwi3_iV_x0e65HKRpg/edit#gid=0
DroidCon 2016 Berlin: Testing Why? When? How?
DroidCon 2016 Berlin: Elegant?? Unit Testing
DroidCon 2016 Berlin: Testing made sweet with a Mockito
DroidCon 2016 Berlin: #Perfmatters for Android