[자바 8-11] 스트림 뿌시기

코린이서현이·2024년 12월 14일
0

Java

목록 보기
50/50
post-thumbnail

스트림에 대해서 정리해보자!
스트림에는 굉장히 많은 메서드가 있고 책에서도 굉장히 많은 스트림 메서드를 다루고 있다. 그러나 모든 메서드를 알아야하는 것은 아니니 중요하고 자주 쓰이는 메서드 위주로 정리해보려고 한다.

스트림 찍먹해보기!

스트림은 컬렉션 데이터, 스트림 데이터, 문자열등의 데이터를 간단하게 다루는 기능이다.

그런데 스트림의 간단이라는 것은 정확히 무엇을 뜻하는 것일까?
아래의 구체적인 상황으로 이해해보자!

요구사항

1. 화장품 가게에서 할인을 하는 제품을 찾아야한다.

//(1) `for`문으로 구현
for (Cosmetic cosmetic : cosmetics) {
	if (cosmetic.getDiscountRate() > 0) {
    	discountedItems.add(cosmetic);
    }
}

//(2) `stream`으로 구현
cosmetics.stream()
	.filter(c -> c.getDiscountRate() > 0)
    .toList;

추가된 요구사항 2. 할인가격이 만원이상인 것만 추출하고 싶다.

(1) `for`문으로 구현
for (Cosmetic cosmetic : cosmetics) {
	if (cosmetic.getDiscountRate() > 0) {
    	if (discountedPrice >= 10000) {
            discountedItems.add(cosmetic);
        }
    }
}

//(2) `stream`으로 구현
cosmetics.stream()
    .filter(c -> c.getDiscountRate() > 0)
    .filter(c -> c.getDiscountedPrice() >= 10000)
    .toList();

1. 보기에 간단하다!

직접 if문과 add등으로 연산을 제어하는 1번보다, 간결하게 표현되는 2번의 방식이 가독성이 좋다.

특히 중첩문이 없다는 것도 장점이다!

2. 사용하기에 간단하다!

스트림은 선언적(선언형)이라는 특징을 가진다.

선언적이라는 것은 마치 sql 질의문처럼, 어떻게 와 같은 자세한 코드없이 이루어진다.
세부구현에 대한 코드가 없기 때문에 중요한 부분에 더 집중할 수 있게 되는 것이다.
위의 예시를 보면 스트림의 사용예시에서는 if문과 같은 제어블록이 사용되지 않은 것을 볼 수 있다.

즉, 사람이 이해하기에 더 간단한 고수준의 특징을 가지는 것이다.
그래서 이런 스트림을 고수준 빌딩 블록이라고 한다.

저수준과 고수준의 뜻을 더 알아보자

  • 저수준: How에 집중해서 더 세세하게 많은 것들을 제어
  • 고수준: What에 집중해서 무엇을 하는지에 대한 것에 집중, 더 추상화된 수준

3. 활용, 유지보수에 간단하다!

아주 완벽한 예시는 아니지만 stream을 사용했을때, 중첩문 없이도, filter() 메서드 하나만 더 추가해서 구현할 수 있었다.

코드 구조가 유지되고, 조건을 추가하는 것이 쉬워 유지보수에 더 편하다.

이제 스트림을 더 알아보자

스트림은 데이터 소스이어붙인 스트림 연산으로 처리하는 연속적인 값의 일회성 객체이다.

  • 데이터 소스 : 스트림이 다루는 일회성 객체의 원 소스를 말한다. 컬렉션, 배열, I/O의 데이터등이 해당된다.
  • 스트림 연산 : 중간연산과 최종연산으로 이루어지고, 이 스트림 연산을 통해서 데이터를 조작할 수 있다. 또한 중간연산의 연결로 파이프라임을 구성할 수 있다.
  • 연속적인 값의 일회성 객체: 스트림 자체를 의미하는데, 이 스트림이 데이터를 연속적으로 처리하는 객체이면서 한 번 사용하면 소비되어 재사용 불가능하기 때문이다.

스트림의 연산에는 두가지 종류가 있다.

스트림은 데이터를 한번만 소비하는 일회성객체이다.
그러나 위의 예시에서는 마치 여러번 소비를 하는 것처럼 느껴질 것이다.
filter는 스트림을 반환하는 중간연산이기 때문이다.

  • 중간 연산은 스트림을 반환하기 때문에, 중간 연산을 연결해서 계속 스트림을 사용할 수 있다. 따라서 데이터를 소비하는 것이 아니다.
	filter(), map(), sorted(), distinct() //중간연산의 예시
  • 최종연산은 최종적으로 결과를 도출하는 연산이다. 스트림의 요소를 소비하기 때문에 최종연산 후에는 또 중간연산을 붙일 수 없다.
	count(), forEach(), collect(), reduce() //최종연산의 예시
  • 추가적으로 파이프라인이라는 것은 스트림 연산의 연결을 말한다.

스트림의 특징

내부반복

다시 위에서 봤던 for문과 stream을 비교해보자.

stream을 사용하지 않을 때는 직접 for문이나 while문으로 반복을 명시적으로 제어해야했다.
그러나 stream은 반복제어자를 사용하지 않고 반복을 실행한다.
이 개념을 내부 반복이라고 한다.

lazy(중간 연산의 호출 시점)

스트림의 특징 중 하나인 lazy는 중간연산이 최종 연산 호출 전에는 실행되지 않는다는 것이다.

List<String> names = Arrays.asList("Kim", "Park", "Lee", "Choi"); 

Stream<String> stream = names.stream()
    .filter(name -> {
        System.out.println("(1) 필터링: " + name);  
        return name.length() > 2;
    })
    .map(name -> {
        System.out.println("(2) 매핑: " + name);    
        return name.toUpperCase();
    });

System.out.println("(3) 중간 연산 정의 완료");     // 여기까지 아무 연산도 실행되지 않음

// 최종 연산 호출 - 이때 모든 연산이 실제로 수행됨
List<String> result = stream.toList);

여기서 최종 연산은 toList이고 filtermap은 중간연산에 해당한다.
중간연산은 최종 호출 전에는 수행되지 않기때문에 3번이 출력되고 1번,2번이 실행되는 것을 확인할 수 있다.

🖥️ 출력화면

(3)중간 연산 정의 완료
(1) 필터링: Kim
(2) 매핑: Kim
(1) 필터링: Park
(2) 매핑: Park
(1) 필터링: Lee
(2) 매핑: Lee
(1) 필터링: Choi
(2) 매핑: Choi

그렇다면 왜 이런 Lazy한 계산을 지원하는 걸까?

  • 효율성: 필요한 시점에 필요한 연산만 수행
  stream.filter(...)  // 이 시점에는 실행되지 않음
        .map(...)     // 이것도 실행되지 않음
        .count();     // 이때 모든 연산이 실행됨
  • 최적화: 전체 파이프라인을 보고 최적화 가능
stream.filter(expensive)      // 비싼 연산
      .limit(5)              // 5개만 필요
      .forEach(print);       // 5개 찾으면 바로 종료
  • 무한 스트림: 무한한 요소도 처리 가능
Stream.iterate(1, n -> n + 1)  // 무한 스트림
      .filter(n -> n % 2 == 0) // 짝수 필터링
      .limit(5)                // 5개로 제한
      .forEach(System.out::println);

for문과 스트림의 비교

그런데 for문으로도 할 수 있는 거 아니야?? 라는 생각이 들 수도 있다.

물론 가능하다. 하지만 for문보다 stream은 위에서 이야기했듯이 세부적인 제어문 없이 무엇을 할지에 초점 맞춰서 코드를 짤 수 있다.

덕분에 가독성이 올라가고, 변경되는 코드에 더 유연하게 대응할 수 있다는 장점을 가진다.

profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글