[LG CNS AM Inspire Camp 1기] Java (4) - 스트림 API

정성엽·2025년 1월 18일
1

LG CNS AM Inspire 1기

목록 보기
29/53
post-thumbnail

INTRO

이번에는 자바의 스트림(Stream)에 대해서 정리해보려고 한다.

이전 포스팅에서 람다식을 정리하면서 스트림 API를 살짝 다뤘었는데, 이번 포스팅에서 본격적으로 알아보자 👀


1. 스트림이란?

스트림은 컬렉션, 배열 등의 저장 요소를 하나씩 참조하면서 함수형 인터페이스(람다식)를 적용하며 반복적으로 처리할 수 있도록 해주는 기능이다.

자바 8에서 추가된 이 기능은 '데이터의 흐름'이라고 생각하면 이해하기 쉽다.

왜 스트림이 필요한지 간단한 예제를 통해 살펴보자!

💡 기존 방식의 문제점

Sample Code

List<String> names = Arrays.asList("김자바", "이파이썬", "박코틀린");
List<String> longNames = new ArrayList<>();

for(String name : names) {
    if(name.length() >= 4) {
        longNames.add(name);
    }
}

for(String name : longNames) {
    System.out.println(name);
}

위 코드는 이름 목록에서 길이가 4 이상인 이름을 찾아 출력하는 간단한 로직이다.

하지만 로직이 복잡해질수록 for문이 중첩되고 코드의 가독성이 떨어진다는 문제점이 있다.

💡 스트림을 활용한 해결

위 코드를 스트림을 사용하면 다음과 같이 작성할 수 있다:

Sample Code

List<String> names = Arrays.asList("김자바", "이파이썬", "박코틀린");

names.stream()
     .filter(name -> name.length() >= 4)
     .forEach(System.out::println);

위 코드처럼 . 을 통해서 스트림의 메서드들을 체이닝하는 것이 바로 스트림을 이용하는 방법이다.

스트림의 특징 중 하나는 이전 연산의 결과가 다음 메서드의 입력이 된다는 것이다.

따라서, .filter(name -> name.length() >= 4) 의 결과가 .forEach(Systm.out::println); 의 매개변수로 전달되는 것이다.

이처럼 스트림을 사용하면 코드의 흐름을 파악하기 쉬워지고 간결해진다는 특징이 있다!


2. 스트림의 구조

필자는 그냥 1번 방법처럼 필요한 메서드들을 가져다가 붙여서 사용하면 되는 줄 알았다.

하지만, 강의를 수강하면서 스트림에도 구조가 있다는 것을 알게되었고, 이제부터 스트림의 구조에 대해서 정리해보려고 한다.

💡 스트림의 동작 구조

스트림은 크게 세 가지 단계로 동작한다.

동작 구조

  • 스트림 생성
  • 중간 연산 (필터링, 매핑 등)
  • 최종 연산 (결과 도출)

참고

위 사진 처럼 스트림은 주로 컬렉션과 배열을 통해서 생성하고, 중간 처리 과정을 거친 이후, 결과를 리턴한다.

이제부터 각각의 단계를 자세히 살펴보자.

💡 스트림 생성

스트림은 다양한 데이터 소스로부터 생성할 수 있다.

코드를 통해 살펴보자

Sample Code

// 1. 컬렉션으로부터 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream();

// 2. 배열로부터 생성
String[] arr = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(arr);

// 3. 숫자 범위로부터 생성
IntStream stream3 = IntStream.range(1, 5); // 1,2,3,4
IntStream stream4 = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

// 4. 직접 값을 지정하여 생성
Stream<String> stream5 = Stream.of("a", "b", "c");

계층 구조

위 코드처럼 . 을통해서 stream()메서드를 호출하여 스트림 인스턴스를 생성할 수 있다.

1번, 2번은 각각 컬렉션과 배열로부터 스트림 인스턴스를 생성하는 예시를 보여준다.

3번과 4번은 계층 구조 사진에 있는 StreamIntStream 인터페이스를 이용해서 인스턴스를 직접 생성하고 있다.

(주로 사용하는 방법은 1번과 2번이다)

💡 중간 연산

중간 연산은 스트림을 다른 스트림으로 변환하는 연산이다.

이전에 소개했던 것처럼 여러 개의 중간 연산을 연결할 수 있다는 특징이 있다.

예제 코드를 통해 자주 사용되는 중간 연산을 한번 살펴보자

Sample Code

// Sample Data
List<String> names = Arrays.asList("김자바", "이파이썬", "박코틀린", "최자바", "김자바");

// 1. filter: 조건에 맞는 요소 선택
names.stream()
     .filter(name -> name.startsWith("김"))
     .forEach(System.out::println);

// 2. map: 요소를 다른 요소로 변환
names.stream()
     .map(String::length)
     .forEach(System.out::println);
     // 3 4 4 3 3 출력

// 3. sorted: 요소 정렬
names.stream()
     .sorted()
     .forEach(System.out::println);
     // 김자바 김자바 박코틀린 이파이썬 최자바 출력

// 4. distinct: 중복 제거
names.stream()
     .distinct()
     .forEach(System.out::println);
     // 김자바 이파이썬 박코틀린 최자바

자주 사용되는 스트림의 중간 연산을 샘플 코드로 정리해봤다.

필자도 아직 모든 중간 연산자가 어떤 기능을 하는지는 모른다.

하지만, 위와 같은 방식으로 사용할 수 있다는 것을 눈에 익혀두면 좋겠다

그리고 기억해야하는 것은 중간 연산은 지연 연산이라는 것이다.

즉, 최종 연산이 호출되기 전까지는 실제로 실행되지 않는다!

💡 최종 연산

최종 연산은 스트림의 요소를 소모하며 결과를 산출한다.

최종 연산이 수행되면 스트림은 닫히고 더 이상 사용할 수 없다.

이전 코드에서 살펴본 .forEach() 도 사실 최종 연산에 포함되는 것이다.

예시 코드를 통해 정리해보자

Sample Code

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

// 1. forEach: 각 요소에 대해 실행
numbers.stream()
       .forEach(System.out::println);

// 2. count: 요소 개수 반환
long count = numbers.stream().count();

// 3. collect: 결과를 컬렉션으로 변환
List<Integer> evenNumbers = numbers.stream()
								   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());

// 4. reduce: 요소를 하나로 줄임
int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);

위 예시 코드에서는 4개의 최종 연산을 수행하는 메서드를 살펴봤다.

물론 이외에도 많은 스트림 API가 존재하기 때문에 많이 알수록 쉽게 코드를 작성할 수 있을 것이다!


3.스트림 활용

실제로 개발을 하다 보면 스트림을 사용하는 경우가 정말 많다.

특히 필자는 데이터를 필터링하고 변환하는 작업에서 자주 사용한다.

Sample Code

private Map.Entry<Post, Integer> findPostStatistics(Post post) {
	return postStatistics.entrySet().stream()
           .filter(entry -> entry.getKey().equals(post))
 		   .findFirst()
 		   .orElse(null);
}

위 코드는 미니 프로젝트에서 필자가 사용한 게시물 통계 조회 기능의 일부분인데, 스트림을 사용하니 코드가 마치 하나의 문장처럼 읽히는 느낌이다.

이 코드를 처음 보는 개발자들도 해당 메서드의 기능이 "통계 데이터에서 해당 게시물을 찾아서 반환하고, 없으면 null을 반환한다"라고 직관적으로 이해할 수 있을 것이다.

이처럼 스트림은 코드의 가독성을 크게 향상시켜준다는 걸 이해할 수 있다.

처음에는 익숙하지 않을 수 있지만, 한번 익숙해지면 코드의 의도를 파악하기가 훨씬 쉬워진다!


OUTRO

이번 포스팅에서는 자바의 스트림에 대해 알아봤다.

스트림은 컬렉션을 함수형으로 처리할 수 있게 해주는 강력한 기능이라고 배웠다.

필자는 처음에 조금 생소했지만, 쓰다보니 직관적으로 코드를 이해할 수 있어서 좋은 기능인 것 같다.
(그런데 성능적 측면에서 스트림이 엄청 느리다고 한다)

아마 앞으로 자바로 개발을 하게되면 스트림을 적극적으로 활용할 것 같다 👊

profile
코린이

0개의 댓글

관련 채용 정보