본문 바로가기
Programming

Java의 List Stream()

by BTC_동동 2023. 11. 24.

안녕하세요 여러분! 일단고 팀입니다.

벌써 크리스마스의 시즌이 느껴지면서 날씨가 추워집니다.

몸과 마음 모두 따뜻하게 해야 할 계절이네요.

 

오늘은 Java의 List 인터페이스에서 제공하는 Stream() 메서드에 대해 알아보고, 이를 활용하여 코드를 간결하게 작성하고 성능을 향상시킬 수 있는 방법에 대해 살펴보겠습니다.

1. Stream 이란

Java 8부터 추가된 Stream은 컬렉션 요소들을 처리하기 위한 연속적인 데이터 흐름을 나타내는 인터페이스입니다. Stream을 활용하면 간단하고 선언적인 방식으로 데이터를 처리할 수 있습니다. 이를 통해 병렬 처리 및 다양한 연산을 수행할 수 있어 코드의 가독성과 유연성이 증가합니다.

 

2. List.stream() 과 foreach 구문

List.stream()과 foreach 구문은 기능적 측면에서 유사한 면이 존재합니다. 저는 개인적으로 stream() 형태로 많은 List 데이터를 처리하는데 stream()의 장점이 있기 때문입니다.

기존의 foreach문과 List.stream()을 사용할 때 성능적인 측면에서 큰 차이가 있습니다. Stream을 활용하면 내부적으로 요소들을 병렬로 처리할 수 있어서 대용량 데이터의 처리에서 더 효율적입니다. 또한, 코드의 가독성과 유지보수성이 높아지는데, 이는 람다 표현식과 다양한 중간 및 최종 연산을 통해 직관적으로 작성할 수 있기 때문입니다.

 

3. List.stream() 사용방법

stream() 메서드를 람다 형태를 통해서 사용하는 방법을 말씀드리겠습니다.

3.1 중간 연산과 최종연산

stream()을 람다식으로 사용할 때 중간연산과 최종연산이란 개념을 알아야 사용할 수 있습니다.

중간 연산과 최종 연산을 유연하게 조합하여 데이터를 처리하는 방법에 대해 살펴보겠습니다.

 

중간 연산 메서드

  1. filter(): 주어진 조건에 따라 요소를 필터링합니다.
  2. map(): 요소를 다른 요소로 매핑하거나 변환합니다.
  3. flatMap(): 여러 Stream을 하나의 Stream으로 평면화하거나 요소를 변환합니다.
  4. distinct(): 중복 요소를 제거합니다.
  5. sorted(): 요소를 정렬합니다.
  6. peek(): 각 요소를 확인하거나 로깅할 때 사용됩니다.
  7. limit(): 처음부터 지정된 개수까지의 요소만 사용합니다.
  8. skip(): 처음부터 지정된 개수만큼 요소를 건너뜁니다.

최종 연산 메서드

  1. forEach(): 각 요소에 대해 작업을 수행합니다.
  2. collect(): 요소를 수집하여 컬렉션 혹은 다른 형태로 변환합니다.
  3. reduce(): 모든 요소를 하나의 값으로 줄여나갑니다.
  4. count(): 요소의 개수를 반환합니다.
  5. anyMatch(), allMatch(), noneMatch(): 조건에 부합하는 요소가 있는지 확인합니다.
  6. findAny(), findFirst(): 요소를 찾아 반환합니다.
  7. min(), max(): 최소값 혹은 최대값을 찾아 반환합니다.
  8. toArray(): Stream을 배열로 변환합니다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Edward");

// 중간 연산(filter, map)과 최종 연산(collect) 조합 예시
List<String> modifiedNames = names.stream()
        .filter(name -> name.length() > 4) // 이름 길이가 4보다 긴 요소만 필터링
        .map(String::toUpperCase) // 대문자로 변환하여 매핑
        .collect(Collectors.toList()); // 변환된 요소들을 리스트로 수집

System.out.println(modifiedNames); // 출력: [CHARLIE, DAVID, EDWARD]

이처럼 중간 연산과 최종 연산을 조합하여 원하는 작업을 구성할 수 있습니다. Stream을 유연하게 활용하여 데이터 처리를 할 때, 적합한 연산들을 적절히 조합하여 사용하면 보다 간결하고 효율적인 코드를 작성할 수 있습니다.

중간 연산이 여러 번 포함

중간 연산을 여러 번 사용해서 원하는 데이터 처리를 한 코드에서 가능하고 그게 stream()의 큰 장점 중 하나라고 생각이 듭니다.

List<String> words = Arrays.asList("apple", "banana", "grape", "orange", "watermelon");

// 중간 연산(filter, map, distinct)이 여러 번 적용된 경우
List<String> result = words.stream()
    .filter(word -> word.length() > 5) // 길이가 5보다 큰 단어 필터링
    .map(String::toUpperCase) // 대문자로 변환
    .distinct() // 중복 제거
    .filter(word -> word.startsWith("A")) // A로 시작하는 단어 필터링
    .collect(Collectors.toList()); // 필터링된 단어들을 리스트로 수집

System.out.println(result); // 결과 출력

 

중간 연산만 존재할 경우

중간 연산만으로 Stream을 마무리하지 않고 최종 연산을 수행하지 않으면, Stream은 실제로 아무런 연산을 수행하지 않습니다. 이는 Java의 Stream이 '게으른(lazy)' 특성을 가지고 있기 때문입니다. 이로 인해 발생할 수 있는 몇 가지 문제점이 있습니다.

  1. 결과를 얻을 수 없음: 중간 연산만으로 Stream 파이프라인을 구성했을 때, 최종 연산을 호출하지 않으면 결과를 얻을 수 없습니다. 최종 연산을 호출하지 않으면 Stream 파이프라인은 실행되지 않으며, 실제 요소들을 처리하거나 연산을 수행하지 않습니다.
  2. 메모리 누수의 가능성: Stream은 데이터 소스와 연결되어 있을 수 있습니다. 따라서 Stream이 닫히지 않고 남아있는 경우 메모리 누수가 발생할 수 있습니다. 특히 무한한 요소를 다루는 경우, 최종 연산을 수행하지 않으면 Stream이 계속해서 메모리를 소비할 수 있습니다.
  3. 프로그래머 실수: 최종 연산을 잊어버리거나 호출하는 곳을 놓치는 경우가 있을 수 있습니다. 이는 예상치 못한 버그의 원인이 될 수 있습니다.
  4. 효율성 문제: 중간 연산만 사용하면서 필요하지 않은 연산을 계속 수행하거나, 중간 결과를 캐시하지 않고 매번 새로운 Stream을 생성하는 등의 효율성 문제가 발생할 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 중간 연산만 사용한 경우
Stream<Integer> modifiedStream = numbers.stream()
        .filter(num -> num % 2 == 0) // 짝수만 필터링
        .map(num -> num * 2); // 각 요소를 2배로 변환

// 최종 연산을 호출하지 않은 상태
// modifiedStream은 아무런 연산을 수행하지 않음

System.out.println(modifiedList);

위 코드에서 중간연산 map() 만 존재하고 최종연산이 존재하지 않습니다. 이럴 경우 위에서 말씀드린 내용에서 원하는 결과를 얻지 못하거나 Stream이 닫히지 않고 남아있어 메모리 누수가 발생합니다. 또한 위의 코드의 경우 로그나 코드상에서 에러를 발견하지 못하기 때문에 결과가 출력되지 않아 전체코드에서 문제점을 찾아내기 쉽지 않게 됩니다. 따라서 최종연산까지 꼭 해줘야 합니다.

 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 중간 연산과 최종 연산을 함께 사용한 경우
List<Integer> modifiedList = numbers.stream()
        .filter(num -> num % 2 == 0) // 짝수만 필터링
        .map(num -> num * 2) // 각 요소를 2배로 변환
        .collect(Collectors.toList()); // 변환된 요소들을 리스트로 수집

System.out.println(modifiedList); // 출력: [4, 8]

collect() 최종연산을 통해 최종적으로 stream 파이프라인에서 list를 반환하게 하여 원하는 결과값을 얻어내고 stream을 최종적으로 닫히게 해줍니다.

 

3.2 Parallel Streams

병렬 스트림(Parallel Streams)은 Java에서 제공하는 기능으로, 스트림의 처리를 병렬로 수행하여 성능을 향상시킬 수 있습니다. 스트림 API는 병렬 스트림을 쉽게 생성할 수 있도록 설계되어 있습니다. 일반 스트림과 달리 데이터를 병렬로 처리하기 때문에 다중 코어를 활용하여 작업을 분산하여 더 빠른 처리 속도를 얻을 수 있습니다.

 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 병렬 스트림으로 변환
Stream<Integer> parallelStream = numbers.parallelStream();

// 병렬 스트림에서의 중간 연산과 최종 연산 조합 예시
List<Integer> result = parallelStream
    .filter(num -> num % 2 == 0) // 짝수만 필터링
    .map(num -> num * 3) // 각 요소를 3배로 변환
    .sorted() // 요소를 정렬
    .collect(Collectors.toList()); // 변환된 요소들을 리스트로 수집

System.out.println(result); // 병렬 스트림으로 처리된 결과 출력

위 코드에서 병렬처리를 했기 때문에 더 빠른 장점이 있지만 병렬 스트림에서의 연산은 기본적으로 순서가 보장되지 않을 수 있으며, 데이터를 병렬로 처리하기 때문에 결과의 순서가 예측하기 어려울 수 있습니다. sorted() 같은 중간연산으로 정렬 하면 역시 정확한 순서를 보장하지 않을 수 있습니다.

 

 

오늘은 java에서 List.stream()에 대해서 알아봤습니다. 도움이 되었으면 좋겠습니다.

연말이 다가오는 만큼 마무리 잘 하셨으면 좋겠습니다.

그럼 다음에 만나요!

'Programming' 카테고리의 다른 글

[Programming] Spring 이란?  (0) 2023.11.24
[Vue]Vue.js 컴포넌트 디자인 패턴  (1) 2023.11.24
[Django/React] 장고와 리액트 연동 (3)  (1) 2023.11.21
[Vue]Vue Router : Hash mode & History mode  (0) 2023.11.17
좋은 주석이란  (1) 2023.11.17

댓글