본문 바로가기
JAVA/Effective Java

item 45. 스트림은 주의해서 사용하라.

by Garonguri 2022. 6. 18.
728x90

스트림이 뭘까요?

 

: 데이터 처리 작업(순차 또는 병렬)을 지원하고자 Java8부터 추가된 API


스트림이 제공하는 추상 개념의 핵심 두가지

 

1. 데이터 원소의 유한 혹은 무한 시퀀스

2. 원소들로 수행하는 연산 단계를 표현하는 스트림 파이프 라인


스트림의 원소는 컬렉션, 배열, 파일, 정규표현식 패턴 매처, 난수 생성기 등 어디로부터든 올 수 있다.

스트림 안의 데이터 원소들은 객체 참조나 int, long, double의 기본 타입 값이다.


스트림 파이프 라인의 생애 주기(?)

 

소스 스트림 -> 1개 이상의 중간 연산 -> 종단 연산

스트림  중간 연산들은 모두 한 스트림을 다른 스트림으로 변화시키는데, 변환된 스트림의 원소 타입 값은 기존과 다를 수 있다.

종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후 연산을 한다.

 

스트림 파이프라인은 지연 평가된다.

: 종단 연산이 수행될 때 평가되며, 종단 연산이 수행되지 않으면 아무일도 일어나지 않는다.

 

 *지연 평가란?

: 그때 그때 값을 평가하지 않고, 정말 결과값이 필요한 시점까지 평가를 미루는 것

1. 필요할 때만 평가가 되므로 메모리를 효율적으로 사용할 수 있다.

2. 무한 자료구조를 만들 수 있음

3. 런타임 에러를 방지 할 수 있다. ( 컴파일 시 에러를 체킹)

4. 컴파일러 최적화 가능

 

따라서, 종단 연산을 꼭 수행해야 무한 스트림을 다룰 수 있다. 종단 연산을 빼먹지 말자!

 


스트림 API는 플루언트 API이다.

  • 메서드 연쇄를 지원한다.
  • 파이프라인 하나를 구성하는 모든 호출을 연결해 하나의 표현식을 만든다.
  • 기본적으로 순차적으로 진행이 된다.
  • 병렬로 실행하기 위해서는 parallel() 을 호출하면 된다. (그러나 효과는 미미)

스트림 좋다~~~~~

그런데 잘못 사용하면 읽기 어렵고 유지보수가 힘들다고 한다.

그럼 언제 사용하면 좋을까?

 

: 스트림을 과용하면 프로그램이 읽기 어려워지고 유지보수하기 힘들어지는 경우가 있다.

 

스트림을 자칫 과용하면,

Map<String, Set<String>> groups = new HashMap<>();
        try (Scanner s = new Scanner(dictionary)) {
            while (s.hasNext()) {
                String word = s.next();
                groups.computeIfAbsent(alphabetize(word),
                        (unused) -> new TreeSet<>()).add(word);
            }
        }

        for (Set<String> group : groups.values())
            if (group.size() >= minGroupSize)
                System.out.println(group.size() + ": " + group);

이러한 코드가

try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(
                    groupingBy(word -> word.chars().sorted()
                            .collect(StringBuilder::new,
                                    (sb, c) -> sb.append((char) c),
                                    StringBuilder::append).toString()))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .map(group -> group.size() + ": " + group)
                    .forEach(System.out::println);
        }

이렇게 복잡해진다. 너무 과용하지 말고 적절히 활용해보자.

try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> alphabetize(word)))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(group -> System.out.println(group.size() + ": " + group));
        }

좀 더 적절히 사용하면 깔끔한 코드를 만들 수 있다.

 


+ 스트림 잘쓰는 노하우!

 

  • char 값들을 처리할 때는 스트림을 사용하지 말자. char의 스트림은 자바는 지원하지 않고 있기 때문이다.
  • 모든 반복문을 스트림으로 바꿀 수 있더라도, 바꾸지 말자.
    • 스트림을 사용할 때는 유지보수와 가독성을 언제나 생각해야 한다. 
    • 기존 코드는 스트림을 사용하도록 리팩터링 하되 새 코드가 더 나아 보일 때만 반영하자.
  • 스트림은 반복 계산을 함수 객체(람다, 메서드 참조)로, 반복문은 코드블록으로 표현한다.
    • 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다. 그러나 람다에서는 final이거나 사실상 final 변수만 가능하며 지역 변수를 수정할 수 없다.
    • 코드 블록에서는 return문이나 break, continue로 반복문을 제어할 수 있다. 그러나  람다는 불가능하다.
    • 위의 로직 이상의 일들을 수행할 때는 스트림을 사용하지 말자.
  • 스트림을 사용해야 하는 상황들
    • 원소들의 시퀀스를 일관되게 변환한다.
    • 원소들의 시퀀스를 필터링한다.
    • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
    • 원소들의 시퀀스를 컬렉션에 모은다
    • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
  • 스트림으로 처리하기 어려운 일들
    • 한 데이터가 파이프라인의 여러 단계를 통과할 때, 이 데이터의 각 단계에서의 값들에 동시 접근하기 어려운 경우
      • 스트림 파이프라인은 일단 한 값을 다른 값에 매핑하고 나면 원래의 값은 잃는 구조이기 때문
    • 원래 값과 새로운 값의 쌍을 저장하는 객체를 사용해 매핑하는 우회 방법 존재
      • 복잡하기 떄문에 사용하지 않는게 좋다.
      • 그냥 매핑을 거꾸로 수행해라.

 

스트림과 반복문은 결국 취향 차이이다. 내 맘대로 하자! (너무 맘대로는 하지 말고)

 

핵심 정리

스트림은 편하고 깔끔하지만, 과용하면 유지보수와 가독성이 나빠지므로 주의해서 사용하자.
반복문을 스트림으로 변경하고 싶다면,
기존 코드는 스트림을 사용하도록 리팩토링 하되, 새 코드가 더 나아 보일때만 반영하자.

스트림과 반복문, 어느 것을 사용해야 할 지 고민된다면 둘 다 해보고 결정하자. 정답은 없으니까!
728x90

댓글