티스토리 뷰

개념

  • 스트림은 파라미터로 제공되는 함수(Function<T, Stream<R>>과 관련된 함수형 인터페이스)를 적용해서 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행하는 flatMap이라는 메서드를 제공한다.

  • 추상적으로 설명하면 차원을 낮추는 개념이라고 생각된다. (ex. Stream<String[]> -> Stream<String>)

    • 위의 예시의 경우 일반 map 메서드를 사용할 경우 String[] 배열 자체가 다른 객체로 변환되는데 flatMap을 사용하면 String[] 내부의 값들 자체를 다른 객체로 변환하는 것이 가능하다.
  • 여기서 말하는 함수는 mapFunction<T, R>과 약간의 차이가 있다.

    • flatMap에서 사용되는 FunctionT는 동일하지만 RStream<R>이다.
    • 즉, 일반 제네릭 타입(R)의 객체가 아니라 일반 스트림(Stream<R>)이나 기본형 특화 스트림(IntStream, LongStream, DoubleStream)과 같이 스트림 타입이 반환되어야 한다.

  • flatMap의 기본 개념은 위의 이미지와 같다.

  • 또한 flatMap은 반환하는 형태에 따라 여러 메서드가 존재한다.

    • flatMapToInt, flatMapToLong, flatMapToDouble이 다음에 해당한다.
    • 일반 스트림에서만 위의 3가지 메서드가 추가로 존재하며 기본형 특화 스트림에서는 flatMap만 존재하고 각각의 타입에 맞는 기본형 특화 스트림을 반환한다.

flatMap 메서드

  • 위에서 설명한 가장 기본적인 형태의 flatMap 메서드다.
  • 일반 스트림과 기본형 특화 스트림 모두에서 제공하는 메서드다.
  • 스트림 별로 flatMap의 파라미터를 살펴보면 다음과 같다.

StreamflatMap 메서드

IntStreamflatMap 메서드

LongStreamflatMap 메서드

DoubleStreamflatMap 메서드

  • 스트림의 종류에 따라 각 메서드의 파라미터로 Function<T, Stream<R>>, IntFunction<IntStream>>, LongFunction<LongStream>>, DoubleFunction<DoubleStream>>을 전달받는다.

flatMapToInt 메서드

  • 일반 스트림을 IntStream으로 변환해주는 메서드다.
  • 일반 스트림에서만 제공하는 메서드다.

  • 위의 이미지처럼 일반 스트림으로 변환하는 flatMap과 다르게 Function<? super T,? extends IntStream> mapper를 파라미터로 전달한다.

flatMapToLong 메서드

  • 일반 스트림을 LongStream으로 변환해주는 메서드다.
  • 일반 스트림에서만 제공하는 메서드다.

  • 위의 이미지처럼 일반 스트림으로 변환하는 flatMap과 다르게 Function<? super T,? extends LongStream> mapper를 파라미터로 전달한다.

flatMapToDouble 메서드

  • 일반 스트림을 DoubleStream으로 변환해주는 메서드다.
  • 일반 스트림에서만 제공하는 메서드다.

  • 위의 이미지처럼 일반 스트림으로 변환하는 flatMap과 다르게 Function<? super T,? extends DoubleStream> mapper를 파라미터로 전달한다.

예제

  • 아래 예제에서 사용되는 내용들은 다양한 상황을 연출하기 위해서 임의로 만들어진 내용이므로 특정 속성에 대해서 왜 저 타입이 사용되었는지 의문을 가지지 말자!

  • 예제 코드에서 사용되고 있는 스펙

    1. Java 15 preview (record라는 새로운 클래스 개념을 사용하기 위해서 해당 프리뷰 버전을 사용. 하위 버전의 경우는 일반 클래스를 생성한 후 getter를 만들고 사용하면 됨) - 참고
    2. JUnit 5
    3. Gradle 6.7
  • 소스 코드

기본 데이터 생성

public record AccessLog(Long no, String ip, LocalDate accessedAt, long responseTime) {
}
public record User(Long no, String id, List<AccessLog> accessLogs) {
}
private final FlatMapUsage flatMapUsage = new FlatMapUsage();
private final List<User> users = List.of(
        new User(1L, "alpha", List.of(
                new AccessLog(1L, "192.168.0.1", LocalDate.of(2015, 11, 30), 10),
                new AccessLog(3L, "192.168.0.2", LocalDate.of(2018, 3, 3), 20),
                new AccessLog(7L, "192.168.0.3", LocalDate.of(2020, 7, 15), 100))),
        new User(2L, "beta", List.of(
                new AccessLog(2L, "192.168.0.4", LocalDate.of(2018, 3, 3), 25),
                new AccessLog(4L, "192.168.0.5", LocalDate.of(2019, 8, 23), 17),
                new AccessLog(6L, "192.168.0.6", LocalDate.of(2020, 5, 1), 80))),
        new User(3L, "gamma", List.of(
                new AccessLog(5L, "192.168.0.7", LocalDate.of(2020, 2, 25), 150),
                new AccessLog(8L, "192.168.0.8", LocalDate.of(2020, 8, 5), 200),
                new AccessLog(9L, "192.168.0.9", LocalDate.of(2021, 1, 5), 55))));

문자열 리스트를 한 글자씩 분리해서 고유 문자들만 반환

  • 주어진 문자열 리스트에서 고유 문자만 추출해서 반환해보자.
public List<String> getAllDistinctStrings(List<String> strings) {
    return strings.stream()
            .map(s -> s.split(""))
            .flatMap(Arrays::stream)
            .distinct()
            .collect(Collectors.toList());
}
  • map 메서드가 Stream<String[]>을 반환하고 flatMap 내부에서 Arrays::stream을 사용해서 String[]Stream<String>으로 변환함과 동시에 Stream<Stream<String>>Stream<String>으로 평면화시킨다.

    • 여기서 flatMap이 아닌 map을 사용할 경우 Stream<String>이 아니라 Stream<Stream<String>>로 변환된다.
@Test
@DisplayName("문자열 리스트를 한글자씩 분리해서 고유 문자들만 반환")
void getAllDistinctStringsTest() {
    final var strings = List.of("Happy", "New", "Year");
    final var expected = List.of("H", "a", "p",  "y", "N", "e", "w", "Y", "r");
    final var result = flatMapUsage.getAllDistinctStrings(strings);
    assertIterableEquals(expected, result);
}
  • 다음과 같은 과정으로 테스트가 성공한다.

특정 날짜 이후에 접속한 유저의 ip 조회

  • 실무에서 사용할 법한 예시를 생각해보자.
public List<String> getIpByAccessedAtAfter(List<User> users, LocalDate accessedAt) {
    return users.stream()
            .map(User::accessLogs)
            .flatMap(l -> l.stream()
                    .filter(a -> a.accessedAt().isAfter(accessedAt))
                    .map(AccessLog::ip))
            .collect(Collectors.toList());
}
  • 유저 정보와 접속 기록 정보를 데이터베이스나 파일에서 가져왔다고 하고 특정 날짜 이후에 접속한 유저의 ip 목록을 조회해보자.
  • map 메서드가 Stream<List<AccessLog>>을 반환하고 flatMap 내부에서 List<AccessLog>map을 사용해서 Stream<String>으로 변환함과 동시에 Stream<Stream<String>>Stream<String>으로 평면화시킨다.
@Test
@DisplayName("특정 날짜 이후에 접속한 유저의 ip 조회")
void getIpByAccessedAtAfterTest() {
    final var accessedAt = LocalDate.of(2020, 1, 1);
    final var expected = List.of("192.168.0.3", "192.168.0.6", "192.168.0.7", "192.168.0.8", "192.168.0.9");
    final var result = flatMapUsage.getIpByAccessedAtAfter(users, accessedAt);
    assertIterableEquals(expected, result);
}
  • 다음과 같은 과정으로 테스트가 성공한다.

모든 유저의 평균 응답 시간 구하기

  • 이번에는 기본형 특화 스트림을 파라미터로 가지는 flatMap 관련 메서드를 사용해보자.
public OptionalDouble getAverageResponseTime(List<User> users) {
    return users.stream()
            .map(User::accessLogs)
            .flatMapToLong(l -> l.stream()
                    .mapToLong(AccessLog::responseTime))
            .average();
}
  • 유저 정보와 접속 기록 정보를 데이터베이스나 파일에서 가져왔다고 하고 모든 유저들의 응답 시간 평균을 구해보자.
  • map 메서드가 Stream<List<AccessLog>>을 반환하고 flatMapToLong 내부에서 List<AccessLog>mapToLong을 사용해서 기본형 특화 스트림인 LongStream으로 변환함과 동시에 Stream<LongStream>>LongStream으로 평면화시킨다.
@Test
@DisplayName("모든 유저의 평균 응답 시간 구하기")
void getAverageResponseTimeTest() {
    final var expected = 73.0;
    final var result = flatMapUsage.getAverageResponseTime(users);
    assertTrue(result.isPresent());
    assertEquals(expected, result.getAsDouble());
}
  • 다음과 같은 과정으로 테스트가 성공한다.

이상 Stream API에서 사용하는 중간 연산 중 처음에 이해하기 어려운 flatMap 관련 메서드에 대해 살펴보았다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함