HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🛁
공부기록
/
📚
책 정리
/
🎇
Item46 - 스트림에서는 부작용 없는 함수를 사용하라
🎇

Item46 - 스트림에서는 부작용 없는 함수를 사용하라

속성
7장
스트림에서는 부작용 없는 함수를 사용하라위의 코드는 무엇이 문제일까?수집기..?
 

스트림에서는 부작용 없는 함수를 사용하라

  • 스트림은 그저 또 하나의 API가 아니라 함수형 프로그래밍에 기초한 패러다임일 뿐이다.
  • 스트림이 제공하는 표현력, 속도, (상황에따라) 병령성을 얻으려면 API는 말할 것도 없고 이 패러다임까지 함께 받아들어야 한다.
  • 스트림 패러다임의 핵심은 일련의 변환으로 재구성 하는 부분이다.
  • 이때 각 변환단계는 가능한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
    • 순수 함수란? 오직 입력만이 결과에 영향을 주는 함수를 말한다. 다른 가변 상태를 참조하지 않고 함수 스스로도 다른 상태를 변경하지 않는다.
    • 이렇게 하려면(중간단계든, 종단단계든) 스트림 연산에 건네는 함수 객체는 모두 부작용(사이드이펙트)가 없어야 한다.
// 주위에서 종종 볼 수 있는 스트림 코드로 텍스트 파일에서 단어별 수를 세어 빈도표를 만드는 코드다. Map<String, Long> freq = new HashMap<>(); try(Stream<String> words = new Scanner(file).tokens()) { words.forEach(word -> { freq.merge(word.toLowerCase(), 1L, Long::sum); }); }

위의 코드는 무엇이 문제일까?

  • 스트림, 람다, 메서드 참조를 사용했고 결과도 올바르다 하지만 절대 스트림 코드라고 말할 수 없다.
  • 스트림 코드를 기장한 반복적 코드다. 스트림 API의 이점을 살리지 못하여 같은 기능의 반복적 코드보다 조금 더 길고, 읽기 어렵고, 유지보수에도 좋지 않다.
  • 이 코드의 모든 종단 연산이 forEach에서 일어나는데, 이때 외부 상태(빈도표)를 수정하는 람다를 실행하면 문제가 생긴다.
    • forEach가 그저 스트림이 수행한 연산 결과를 보여주는 일 이상을 하는것(이 예제에서는 람다가 상태를 수정함)을 보니 나쁜 코드일 것 같은 냄새가 난다.
    • Map<String, Long> freq; try(Stream<String> words = new Scanner(file).tokens()) { freq = words.collect(groupingBy(String::toLowerCase, counting())); }
    • 위의 수정된 코드는 올바르다. 처음 예제와 같은 일을 하지만, 이번엔 스트림 API를 제대로 사용했다고 말할 수 있다. 그 뿐만 아니라 짧고 더 명확하다.
    • 하지만 가장 처음에 짯던 방식으로 짜는 사람도 많을 것이다. 익숙하기 때문이다.
    • 자바 프로그래머라면 for-each 반복문을 사용할 줄 알텐데, 종단 연산과 비슷하게 생겼다.
    • 하지만 forEach 연산은 종단 연산 중 기능이 가장 적고 가장 “덜" 스트림답다. 대놓고 반복적이라서 병렬화할 수도 없다.
    • forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자 물론 가끔은 스트림 계산 결과를 기존 컬렉션에 추가하는 등의 다른 용도로는 쓸 수 없다.
 

수집기..?

  • 위의 코드는 Collector 라는 수집기를 사용하는데 스트림을 사용하려면 꼭 ! 꼭꼭 ! 배워야 한다.
  • java.util.stream.Collectors 클래스는 메서드를 무려 39개나 가지고 있고 그 중에는 타입 매개변수가 5개나 되는 것도 있다. 다행히 복잡한 세부 내용을 잘 몰라도, 이 API의 장점을 대부분 활용할 수 있다.
  • 익숙해지기 전까지는 Collector 인터페이스를 잠시 잊고 그저 축소(redution) 전략을 캡슐화한 블랙박스 객체라고 생각하기 바란다.
  • 여기서 축소는 스트림의 원소들 객체 하나에 취합한다는 뜻이다. 수집기가 생성하는 객체는 일반적으로 컬렉션이며 그래서 Collector라는 이름을 쓴다.
  • 수집기를 이용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다. 수집기는 총 세가지로 toList(), toSet(), toCollection(collectionFactory)가 그 주인공이다. 이들은 차례로 리스트, 집합, 프로그래머가 지정한 컬렉션 타입을 반환한다.
  • 지금까지 배운 지식을 활용해 빈도표에서 가장 흔한 단어 10개를 뽑아내는 스트림 파이프라인을 생각해보자.
List<String> toTen = freq.keySet().stream() .sorted(comparing(freq::get).reversed()) .limit(10) .collect(toList()); // 마지막 toList는 Collectors의 메서드다 이처럼 collectors의 멤버를 정적 임포트하여 쓰면 스트림 파이프라인 가독성이 좋아져 흔히 많이 이렇게 사용한다.
  • 이 코드에서 어려운 부분운 sorted에 넘긴 비교자, 즉 comparing(freq::get).reversed() 뿐이다
  • comparing 메서드는 키 추출 함수를 받는 비교자 생성 메서드다. 그리고 한정적 메서드 참조이자 여기서 키 추출 함수로 쓰인 freq::get은 입력받은 단어 키를 빈도표에서 찾아 그 빈도를 반환한다.
  • 그런 다음 가장 흔한 단어가 위로 오도록 비교자를 역순으로 정렬하고 10개를뽑아 리스트에 담는 행위이다.