[Android] 100% 초보자를 위한 RxJava(RxJava for 100% beginners)-part 1

[Android] 100% 초보자를 위한 RxJava(RxJava for 100% beginners)-part 1

이 글은 원문을 번역한 글입니다.

RxJava for 100% beginners(part 1)

요즈음 많은 Android 개발자들이 “Reactive Programming/RxJava”의 존재를 잘 알고 있다고 생각합니다. 최근 Android Weekly에는 매 주 RxJava와 관련된 블로그 포스트가 항상 있기도 했습니다.

하지만, RxJava의 문법에 사용된 함수형 프로그래밍에 대해 잘 모르는 초보자에게는 조금 어려울 수 있습니다. Scala에서는, RxJava의 map(), filter(), flatmap() 등과 유사한 오퍼레이터들이 있습니다. 만약 이전에 Scalar나 (RxJava가 개념과 문법을 차용한) 다른 함수형 프로그래밍 언어를 사용해봤다면, 아예 써보지 않은 사람보다는 나을 수도 있습니다. 이 시리즈에서는, 함수형 프로그래밍을 아예 모르는 분들을 위해 함수형 프로그래밍의 기본부터 다루려고 합니다. 다행히 Java8부터 Stream API가 추가됐고, “Stream” 클래스는 RxJava의 “Observable”과 매우 유사합니다. 그래서 RxJava의 Observable을 이해하기 위해 Java8의 Stream API의 몇몇 요소를 함께 살펴볼 것입니다.

그럼 이제 시작하겠습니다!

초보자분들을 위해, 함수형 프로그래밍의 2가지 주요 이점부터 보면:

  1. 함수는 다른 함수의 파라미터가 될 수 있고, 이것을 “first-class object”라고 합니다.
  2. 함수형 프로그래밍은 operation을 수학에서의 “함수”처럼 다룹니다. -> 우리는 마지막 함수의 결과나 반환값에만 신경쓰면 됩니다. (상태 변화나 객체의 변경을 걱정할 필요가 없습니다.)


RxJava와 Java8 Stream API는 람다 표현식을 지원합니다. 람다 표현식은 위의 첫번째 장점을 잘 나타냅니다. 예시와 함께 보겠습니다:

interface NewAction{
	void call();
}

public static void execute(NewAction action){
  action.call();
}
public static void main(String[] args) {

  execute( new NewAction() {

      @Override
      public void call() {
          // TODO Auto-generated method stub
          System.out.println("action start");
      }
  } );


  execute(()->System.out.println("action start"));
}


line 11 execute( new NewAction(... 과 line 21 execute(()->... 의 차이점을 보면, 둘 다 “action start”를 출력하지만, line 21이 훨씬 짧습니다. 이제 “NewAction” 인터페이스의 인스턴스를 생성하고 “call” 메서드를 오버라이드할 필요가 없습니다. 메서드 바디에 코드를 작성하고 execute()로 전달만 하면 됩니다. That’s not magic!

Java 8을 사용하면, 람다 표현식을 마음껏 쓸 수 있습니다. 하나의 메서드만 포함하는 인터페이스가 필요할 때 코드에서 인터페이스 인스턴스를 직접 생성하지 않고, 메서드 바디를 원하는 곳에 작성하면 됩니다.

그러나, 여기서 함수가 정말 파라미터처럼 사용되는 건 아닙니다. 단지 적은 코드가 가능하게 해주고, 함수를 파라미터처럼 전달하는 것처럼 보이게 하는 컴파일러의 기능입니다.

자, 그럼 2번 장점의 예시를 아래에서 살펴보겠습니다!


public static void main(String[] args) {
	ArrayList<Integer> arrayList = new ArrayList<Integer>();
	arrayList.stream()
	         .map(integer -> integer + 1)
	         .filter(integer -> integer < 10)
	         .limit(10);
}


위 예제에서, ArrayList를 Stream(Java의 InputStream이나 OutStream 같은 개념이 아닙니다)으로 변환하고, 다른 연산자에 적용하여 Stream을 수정했습니다. 먼저 Stream(Integer 객체의 스트림)의 모든 요소에 1씩 더하도록 map을 적용하고, 10보다 작은 정수로 필터링한 후, 앞에서 10개의 요소만 스트림에서 가져왔습니다. 연산자를 적용할 때마다 새로운 스트림이 “생성”되며, 그래서 연산자들을 연쇄로 적용할 수 있습니다.

그런데 잠깐.. 이런 특이한 구조를 써서 좋은 점은 뭘까요?



앞에서 이미 함수형 프로그래밍 언어에 대해 언급을 했었는데, “상태”를 갖지 않고 함수 실행의 결과에만 집중할 수 있도록 하는 것이 목적입니다.

예를 들어 아래와 같은 함수 실행 순서가 있다고 가정하면:


funcA() -> funB() -> funcC();


이 경우, funC()는 funcB()의 결과만 다루고, funcA()의 결과는 사용하지 않을 것입니다. 이것이 가장 이상적인 함수형 프로그래밍에서의 시나리오입니다.

대부분 사람들은 이런식으로 구현할 것입니다:

private boolean flag;

void A(){
	flag = true;
	//do something
	B();
}
void B(){
	C();
}
void C(){
	if(flag){
		//do something
	}
}


이 예제에서, C()의 실행을 컨트롤하기 위해 전역변수를 사용했습니다. 하지만 변수는 변할 수 있고 A() 안에서도 바뀔 수 있습니다. 그래서 함수 실행 순서가 A()->B()->C()라고 해도, C()는 B()에 대해 순수한 상태가 아니며 함수형 프로그래밍의 룰에도 어긋납니다.

코드를 살짝 바꿔볼까요.

void A(){
	boolean flag = true;
	//do something
	B(flag);
}
void B(boolean flag){
	C(flag);
}
void C(boolean flag){
	if(flag){
		//do something
	}
}


훨씬 낫네요! B()와 C()는 더이상 전역변수에 영향을 받지 않고, B()와 C()의 결과는 순수하게 각각의 파라미터에 의해서 결정됩니다.

그리고 “RxJava”스럽게 바꾼다면

public static void main(String[] args) {
	new Exe(true)
	.A()
	.B()
	.C();
}

static class Exe {
	boolean flag;
	public Exe(boolean flag){
		this.flag = flag;
		//do something
	}
	Exe A(){
		boolean flag = true;
		//do something
		return new Exe(flag);
	}
	Exe B(){
		return new Exe(flag);
	}
	Exe C(){
		if(flag){
			//do something
		}
		return new Exe(flag);
	}
}

위 코드는 우리가 어떻게 더 functional하게 만들 수 있는지 보여줍니다!

그러나 여기서 의문점은 -> 여전히 Exe 클래스에 boolean 플래그가 있다는 것입니다! 함수형 프로그래밍 원칙에 어긋나지 않을까요?

음, 그렇진 않습니다. 메서드 체인의 실행을 통제하기 위해 변할 수 있는 변수를 원하지 않는다면 함수형 프로그래밍 원칙에 어긋난다는 것을 항상 기억하세요. 하지만 이 말이 전역변수를 사용할 수 없다는 의미는 아닙니다. 위의 코드에서, A(), B(), C()를 실행할 때마다 Exe클래스의 새로운 인스턴스를 반환하여 같은 Exe 인스턴스를 공유하지 않습니다. 이 말은 바로 이전의 Exe 인스턴스만 현재 메서드의 실행에 관여한다는 의미입니다.

그럼 이제 Java8의 Stream API를 다시 살펴보겠습니다. map(), filter(), limit() 같은 연산자가 새로운 “Stream”을 생성해 다음 연산자에 넘겨줍니다. 이전 연산자에 의해 만들어진 스트림을 변경하기만 하면 됩니다.

public static void main(String[] args) {
	ArrayList<Integer> arrayList = new ArrayList<Integer>();
	arrayList.stream()
	         .map(integer -> integer + 1)
           //at this point, stream has already been modified by adding 1 to each element
	         .filter(integer -> integer < 10)
           //at this point, stream has already been modified by filtering out those numbers less than 10
	         .limit(10);
}


여기까지가 part1 내용입니다. 다음 포스트에서는 RxJava의 연산자와 API를 다루겠습니다.



Comments