티스토리 뷰

Stream에서 중요한 것은 순서이다.

  

  이번에 게임 추천 시스템을 만들면서 알고리즘에 대한 계산 시간을 줄이기 위해 많은 방법을 생각했었다. 그 중에 하나는 싱글 스레드로 작동되는 Node.js의 V8 엔진을 멀티 스레드처럼 작동시켜 프로세스의 작업 효율을 높히는 것이었다. 물론 Parallel.js같이 멀티 프로세싱으로 만들어주는 라이브러리도 있었지만, 필자는 멀티 프로세싱에 대한 이해도 떨어질 뿐만 아니라 이것을 배워 사용하기에는 시간이 많지는 않았다. 그래서 다른 방법을 찾아야 했는데, 그 때 생각한 것이 비동기 방식이었다.


  물론, 비동기 방식은 멀티스레드나 멀티 프로세싱처럼 병렬성을 가지는 것은 아니지만, 동시성을 이용할 수 있었다. 또한, 비동기를 조금 더 잘 활용하기 위해서 Rxjs를 이용할 필요가 있었다. Rxjs는 비동기 작업을 고차원 함수들을 이용해 함수형 언어처럼 개발 할 수 있다. 때문에, 이를 이용해 게임 추천 시스템의 알고리즘을 개발할 수 있었다.


  이번 프로젝트를 수행하면서 Rxjs에 대해 조금 더 깊게 이해 할 수 있는 계기가 되었는데 그 중에 가장 크게 느낀 부분은 mergeMap과 concatMap의 차이점에 대해 많이 배웠고, 이 글의 부제인 mergeMap VS concatMap을 쓰게된 이유이다.


  먼저, MergeMap과 ConcatMap이 어떻게 생겼고, 어떤 일을 하는지에 대해서 살펴보자.


mergeMap 동작 방식


  위 사진은 mergeMap의 동작 방식이다. 한 스트림에서 들어온 데이터가 미리 정의된 함수에 따라 새로운 스트림을 구성하게 되는데 그것을 새로운 스트림 순서에 맞춰 원래 스트림을 반환 하는 구조를 가지고 있다.


concatMap 작동 방식


  위 사진은 concatMap의 작동 방식이다. mergeMap과 동일하게 한 스트림에서 들어온 데이터가 미리 정의된 함수에 따라 새로운 스트림을 구성하고 그것을 원 스트림 순서에 맞춰 원래 스트림을 반환하는 구조를 가지고 있다.


  두 함수에 대한 설명에서 다른 점을 찾을 수 있겠는가? mergeMap은 새로운 스트림의 순서를 따르고 concatMap은 원 스트림의 순서에 기준을 두고 스트림이 반환된다. 이 두 함수의 차이는 크지 않아 보이지만 아주 큰 차이를 보인다. 아래 예제 코드를 보자.


const { from, interval } = require('rxjs');
const { mergeMap, concatMap, take, tap } = require('rxjs/operators');
from([0, 1, 2, 3, 4]).pipe(
mergeMap(data =>
interval(data > 2 ? 1000 : 3000).pipe(
take(1),
tap(() => console.log(data))
)
)
).subscribe();
// result
3 --- 1 sec
4 --- 1 sec
0 --- 3 sec
1 --- 3 sec
2 --- 3 sec
view raw mergeMap hosted with ❤ by GitHub

  위 코드는 mergeMap에 대한 예제이다. 코드를 보게 되면 원래 스트림은 0, 1, 2, 3, 4가 순서대로 스트림에 들어가있는 걸 볼 수가 있다. 이후 mergeMap에 정의된 함수에 의해 데이터가 2 초과일 때는 1초의 Interval을 가지고 2 이하 일 때는 3초의 Interval을 가지게 된다. 그 후 반환된 스트림을 subscribe 하게 되면 1초에서 3, 4가 동시에 출력이 되게 되고 잠시 후 0, 1, 2가 동시에 출력 된다. 이 같이, megeMap은 새로 스트림을 구성하였을 때, 이후에 들어온 데이터가 먼저 들어온 데이터 보다 더 빠른 시간 순서를 가지게 된다면, 반환되는 스트림에서 순서가 바뀌게 된다.


const { from, interval } = require('rxjs');
const { mergeMap, concatMap, take, tap } = require('rxjs/operators');
from([0, 1, 2, 3, 4]).pipe(
concatMap(data =>
interval(data > 2 ? 1000 : 3000).pipe(
take(1),
tap(() => console.log(data))
)
)
).subscribe();
// result
0 --- 3 sec
1 --- 6 sec
2 --- 9 sec
3 --- 10 sec
4 --- 11 sec
view raw concatmap hosted with ❤ by GitHub

  그에 비해 concatMap의 경우에 결과값을 보게 되면 0, 1, 2, 3, 4가 순서대로 출력되는데 출력되는 시간을 보게 되면, 첫 3초 후에 0이 출력되고 또 3초 후에 1, 또 3초 후에 2, 1초 후에 3, 마지막 1초 후에 4가 출력되는 것을 볼 수 있다. 이는 concatMap이 새로운 스트림의 순서보다 원래 스트림 순서를 우선순위에 두기 때문이다.


  때문에, 원 데이터의 순서가 반드시 보장되어야 하는 경우에 mergeMap을 사용하게 되면 순서가 보장 되지 않기 때문에 원하지 않는 결과 값을 얻게 될 수 있다. 이 경우에는 concatMap을 사용해 원 데이터의 순서를 보장해 주어야 한다. 하지만, 원 데이터의 순서가 중요하지 않고, 더 빠른 처리 속도를 원한다면 mergeMap을 사용하는 것이 좋다. 


  필자의 경우에 게임 추천 시스템에서 예상 평점을 계산하는 알고리즘에서 이전에는 concatMap을 이용해 모든 알고리즘을 구현 했었는데 동일한 환경에서 처리 속도가 평균 7 ~ 8초대가 나왔었다. 하지만 순서가 필요 없는 곳에서 mergeMap으로 바꾸는 것만으로도 5초 이내로 줄일 수 있었고, 데이터 전처리 과정을 거쳐 2초 대의 계산 속도를 가질 수 있게 되었다.


  만약, 이전 버전에서 flatMap을 사용했던 개발자라면 현재 버전에서는 flatMap이 사라졌으니 사용 용도에 맞게 mergeMap과 concaMap으로 바꿔주어야한다.

댓글
Total
Today
Yesterday
공지사항
최근에 올라온 글
최근에 달린 댓글