주요글: 도커 시작하기
반응형

시퀀스 변환

이 글에서는 시퀀스가 발생하는 데이터를 변환하는 몇 가지 방법을 살펴본다.


1-1 변환: map

첫 번째는 map()이다. map() 한 개의 데이터를 1-1 방식으로 변환해준다. 자바 스트림의 map()과 유사하다. 다음 코드는 map()의 예를 보여준다.


Flux.just("a", "bc", "def", "wxyz")

        .map(str -> str.length()) // 문자열을 Integer 값으로 1-1 변환

        .subscribe(len -> System.out.println(len));


1-n 변환: flatMap

flatMap은 1개의 데이터로부터 시퀀스를 생성할 때 사용한다. 즉 1-n 방식의 변환을 처리한다. 다음은 간단한 flatMap() 사용 예이다.


Flux<Integer> seq = Flux.just(1, 2, 3)

             .flatMap(i -> Flux.range(1, i)) // Integer를 Flux<Integer>로 1-N 변환


seq.subscribe(System.out::println);


flatMap()에 전달한 함수를 보면 Integer 값을 받아서 다시 1부터 해당 값 개수만큼의 숫자를 생성하는 Flux를 생성한다. 위 코드를 보면 다음과 같은 변환이 발생한다.

  • 1 -> Flux.range(1, 1) : [1] 생성
  • 2 -> Flux.range(1, 2) : [1, 2] 생성
  • 3 -> Flux.range(1, 3) : [1, 2, 3] 생성
flatMap에 전달한 함수가 생성하는 각 Flux는 하나의 시퀀스처럼 연결된다. 그래서 flatMap()의 결과로 생성되는 Flux의 타입이 Flux<Flux<Integer>>가 아니라 Flux<Integer>이다.

실제 실행 결과는 다음과 같다.

1
1
2
1
2
3


걸러내기: filter

filter()를 이용해서 시퀀스가 생성한 데이터를 걸러낼 수 있다. filter()에 전달한 함수의 결과가 true인 데이터만 전달하고 false인 데이터는 발생하지 않는다. 다음은 1부터 10 사이의 숫자 중에서 2로 나눠 나머지가 0인 (즉 짝수인) 숫자만 걸러내는 예를 보여준다.


Flux.range(1, 10)

        .filter(num -> num % 2 == 0)

        .subscribe(x -> System.out.print(x + " -> "));


실행 결과는 다음과 같다.


2 -> 4 -> 6 -> 8 -> 10 -> 


빈 시퀀스인 경우 기본 값 사용하기: defaultIfEmpty

시퀀스에 데이터가 없을 때 특정 값을 기본으로 사용하고 싶다면 defaultIfEmpty() 메서드를 사용하면 된다. Mono와 Flux 모두 defaultIfEmpty()를 제공한다. 아래 코드는 사용 예이다.


Flux<Item> popularItems = getPopularItems();

Flux<Item> recItems = popularItems.defaultIfEmpty(featureItem);


getPopularItems() 메서드가 인기 상품 목록을 제공한다고 하자. 이 코드는 인기 상품 목록을 제공하는 시퀀스인 popularItems가 데이터가 없는 빈 시퀀스면 featureItem을 값으로 제공하는 Flux로 변환한다. 즉 인기 상품이 있으면 그 상품 목록을 제공하고 그렇지 않으면 미리 지정한 featureItem을 값으로 제공하는 Flux를 생성한다.


위 코드를 조금 더 간결하게 표현하면 다음과 같다.


Flux<Item> recItems = getPopularItems().defaultIfEmpty(featureItem);


빈 시퀀스인 경우 다른 시퀀스 사용하기: switchIfEmpty

시퀀스에 값이 없을 때 다른 시퀀스를 사용하고 싶다면 switchIfEmpty() 메서드를 사용한다.


// public Flux<Item> getPopularItems() { ... }

// public Flux<Item> getFeatureItems() { ... }


Flux<Item> recItems = 

    getPopularItems().switchIfEmpty(getFeatureItems());



특정 값으로 시작하는 시퀀스로 변환: startWith

특정 값으로 시작하도록 시퀀스를 설정하고 싶다면 startWith(T ...) 메서드나 startWith(시퀀스) 메서드를 사용한다.


Flux<Integer> seq1 = Flux.just(1, 2, 3);


Flux<Integer> seq2 = seq1.startWith(-1, 0);

seq2.subscribe(System.out::println);


이 코드에서 seq1은 1, 2, 3을 생성하는 시퀀스인데 startWith(-1, 0)을 사용해서 -1, 0으로 시작하는 시퀀스로 변환했다. 따라서 seq2가 생성하는 데이터는 -1, 0, 1, 2, 3이 된다.


특정 값으로 끝나는 시퀀스로 변환: concatWithValues

시퀀스가 특정 값으로 끝나도록 변환하고 싶다면 concatWithValues(T ...) 메서드를 사용한다.


Flux<Integer> seq = someSeq.concatWithValues(100);

seq.subscribe(System.out::println);


위 코드는 someSeq가 어떤 값을 생성하는지에 상관없이 seq는 가장 마지막에 100을 생성한다.


시퀀스 순서대로 연결: cancatWith

concatWith() 메서드를 사용하면 여러 시퀀스를 순서대로 연결할 수 있다.


Flux<Integer> seq1 = Flux.just(1, 2, 3);

Flux<Integer> seq2 = Flux.just(4, 5, 6);

Flux<Integer> seq3 = Flux.just(7, 8, 9);


seq1.concatWith(seq2).concatWith(seq3).subscribe(System.out::println);


위 코드에서 seq1, seq2, seq3을 차례대로 연결하고 구독을 시작했다. 실행 결과는 1부터 9까지 정수를 출력한다.


concatWith로 연결한 시퀀스는 이전 시퀀스가 종료된 뒤에 구독을 시작한다. 위 예에서는 seq1이 종료된 뒤에 seq2 구독을 시작하고 seq2가 종료된 뒤에 seq3 구독을 시작한다.


시퀀스 발생 순서대로 섞기: mergeWith

시퀀스의 연결 순서가 아니라 시퀀스가 발생하는 데이터 순서대로 섞고 싶다면 mergeWith()를 사용한다. 다음은 예이다.


Flux<String> tick1 = Flux.interval(Duration.ofSeconds(1)).map(tick -> tick + "초틱");

Flux<String> tick2 = Flux.interval(Duration.ofMillis(700)).map(tick -> tick + "밀리초틱");

tick1.mergeWith(tick2).subscribe(System.out::println);


위 코드에서 tick1은 1초 간격으로 데이터를 발생하고 tick2는 700 밀리초 단위로 데이터를 발생한다. 이 두 시퀀스를 mergeWith()로 섞은 뒤 구독하면 두 시퀀스를 동시에 구독한다. 실행 결과는 다음과 같다.


0밀리초틱

0초틱

1밀리초틱

1초틱

2밀리초틱

3밀리초틱

2초틱

4밀리초틱

3초틱

5밀리초틱

...


다음은 실행 결과를 발생 시점 기준으로 그림으로 표시한 것이다. 1초 간격으로 데이터를 발생하는 seq1과 0.7초 간격으로 데이터를 발생하는 seq2를 mergeWith()로 섞을 때 데이터 발생 순서를 알 수 있다.





시퀀스 묶기: zipWith

zipWith()를 사용하면 두 시퀀스의 값을 묶은 값 쌍을 생성하는 시퀀스를 생성할 수 있다. 다음은 사용 예이다.


Flux<String> tick1 = Flux.interval(Duration.ofSeconds(1)).map(tick -> tick + "초틱");

Flux<String> tick2 = Flux.interval(Duration.ofMillis(700)).map(tick -> tick + "밀리초틱");

tick1.zipWith(tick2).subscribe(tup -> System.out.println(tup));


zipWith()는 개수를 맞춰서 두 시퀀스의 데이터를 묶는다. 따라서 위 코드는 아래 그림과 같이 쌍을 묶는다.


zipWith()는 리액터에 포함된 Tuple2 타입을 이용해서 두 값을 쌍으로 묶는다. 위 코드의 실행 결과는 아래와 같다.

[0초틱,0밀리초틱]
[1초틱,1밀리초틱]
[2초틱,2밀리초틱]
[3초틱,3밀리초틱]
[4초틱,4밀리초틱]

시퀀스 묶기: combineLatest


Flux.combineLatest() 메서드로 시퀀스를 묶을 수도 있다. 이 메서드 정적 메서드이다. 발생한 개수를 맞춰서 쌍을 만드는 zipWith()와 달리 combineLatest()는 가장 최근의 데이터를 쌍으로 만든다. 다음은 그 차이를 보여준다.



다음은 예제 코드이다.


Flux<String> tick1 = Flux.interval(Duration.ofSeconds(1)).map(tick -> tick + "초틱");

Flux<String> tick2 = Flux.interval(Duration.ofMillis(700)).map(tick -> tick + "밀리초틱");

Flux.combineLatest(tick1, tick2, (a, b) -> a + "\n" + b).subscribe(System.out::println);


지정한 개수/시간에 해당하는 데이터만 유지: take, takeLast

시퀀스에서 처음 n개의 데이터만 유지하고 싶다면 take(long) 메서드를 사용한다. 비슷하게 지정한 시간 동안 발생한 데이터만 유지하고 싶다면 take(Duration) 메서드를 사용한다. 마지막 n개의 데이터만 유지하고 싶다면 takeLast(long) 메서드를 사용한다.

Flux<Integer> seq1 = someSeq.take(10); // 최초 10개 데이터만 유지
Flux<Integer> seq2 = someSeq.take(Duration.ofSeconds(10)); // 최초 10초 동안 데이터 유지
Flux<Integer> seq3 = someSeq.takeLast(5); // 마지막 5개 데이터만 유지

이 외에 takeWhile()과 takeUntil()도 있다. 이 두 메서드는 Predicate을 인자로 받는다. takeWhile()은 Predicate 구현이 true를 리턴하는 동안 데이터를 포함하고 takeUntil()은 처음 true를 리턴할 때까지 데이터를 포함한다.

지정한 개수/시간만큼 데이터 거르기: skip, skipLast

시퀀스에서 처음 n개의 데이터를 거르고 싶다면 skip(long) 메서드를 사용한다. 지정한 처음 시간 동안 발생한 데이터를 거르고 싶다면 skip(Duration) 메서드를 사용한다. 마지막 n개의 데이터를 거르고 싶다면 skipLast(long) 메서드를 사용한다.

take()와 비슷하게 skipWhile()과 skipUntil()을 지원한다.


관련 글


+ Recent posts