책/이펙티브 자바

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

함께자라기 2022. 4. 11. 17:06

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


스트림 API는 다량의 데이터 처리 작업(순차적이든 병렬적이든)을 위해 java8에 추가됐다

스트림 API가 제공하는 추상 개념 중 핵심은 두가지다

  • 스트림(stream)은 데이터 원소의 유한 혹은 무한 시퀀스(sequence)를 뜻한다
  • 스트림 파이프라인(stream pipeline)은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다

스트림의 원소들은 어디로부터든 올 수 있으며 대표적으로 컬렉션, 배열, 파일, 정규표현식 패턴 매처(matcher),난수 생성기, 혹은 다른 스트림이 있다

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

  • 기본 타입 값으로는 int, long, double 세가지를 지원한다

스트림 파이프라인은 소스 스트림에서 시작해 종단 연산으로 끝나며 그 사이에 하나 이상의 중간 연산이 있을 수 있다

  • 각 중간 연산은 스트림을 어떠한 방식으로 변환한다
    • 예로 각 원소에 함수를 적용하거나 특정 조건을 만족 못하는 원소를 걸러낼 수 있다
  • 중간 연산들은 모두 한 스트림을 다른 스트림으로 변환하는데 변환된 스트림의 원소 타입은 변환 전 스트림의 원소 타입과 같을 수도 있고 다를 수도 있다
  • 종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후의 연산을 가한다
    • 원소를 정렬해 컬렉션에 담거나 특정 원소 하나를 선택하거나 모든 원소를 출력하는 식이다

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

  • 평가는 종단 연산 호출시 이뤄진다
  • 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다
  • 이런 지연 평가가 무한 스트림을 다룰 수 있게 해주는 열쇠다
  • 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인 no-op와 같으니 종단 연산을 빼먹지 말자

스트림 API는 메서드 연쇄를 지원하는 플루언트 API다

  • 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다
  • 파이프라인 여러 개를 연결해 표현식 하나로 만들 수도 있다

스트림 파이프라인은 순차적으로 수행된다

  • 파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서 parallel 메서드를 호출하면 된다
    • 막상 효과를 볼 수 있는 상황이 그리 많지가 않다

스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있다

  • 계산을 할 수 있는거지 꼭 해야 하는건 아니다
  • 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해진다
  • 반대로 잘못 사용하면 읽기 어렵고 유지보수도 힘들어 진다

스트림을 처음 사용하면 모든 반복문을 스트림으로 바꾸고 싶은데 서두르지 말자

  • 스트림으로 바꾸는게 가능해도 오히려 가독성과 유지보수성이 떨어지는 경우가 있을 수 있다
  • 중간 정도 복잡한 작업에도 스트림과 반복문을 적절하게 조합하는게 좋다
  • 기존 코드는 스트림을 사용하도록 리팩터링을 해보고 새 코드가 더 나아 보이면 그걸로 사용하자

스트림 파이프라인은 되풀이되는 계산을 함수 객체(람다나 메서드 참조)로 표현하고 반복 코드에서는 코드 블록을 사용해 표현한다

  • 함수 객체로 할수 없지만 코드 블록에선 가능한 작업 (람다로는 모두 불가능하다, 계산 로직에서 아래의 일을 수행해야 되면 스트림과 맞지 않다)
    • 코드 블록에서는 범위 안의 지역변수를 읽고 수정 가능
    • 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있으며 지역변수를 수정할 수 없다
    • 코드 블록에서는 return 문을 사용해 메서드에서 빠져 나갈 수 있다
    • break가 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수도 있다
    • 메서드 선언에 명시된 검사 예외를 던질 수도 있다
  • 스트림을 사용하면 좋은 작업(아래와 같은 일을 수행하는 작업은 스트림을 사용하기에 좋다)
    • 원소들의 시퀀스를 일관되게 변환한다
    • 원소들의 시퀀스를 필터링한다
    • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다(더하기, 연결하기, 최솟값 구하기 등...)
    • 원소들의 시퀀스를 컬렉션에 모은다(공통된 속을 기준으로 묶는다)
    • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다
  • 스트림으로 처리하기 어려운 작업
    • 한 데이터가 파이프라인의 여러 단계(stage)를 통과할 때 이 데이터의 각 단계에서의 값들에 동시에 접근하는 경우
      • 스트림 파이프라인은 일단 한 값을 다른 값에 매핑하고 나면 원래의 값은 잃는 구조다
        • 원래 값과 새로운 값의 쌍을 저장하는 객체를 사용해 매핑하는 우회적인 방법도 있으나 별로 만족스럽지는 않다
        • 매핑 객체가 필요한 단계가 여러곳이면 더욱 별로다 (코드의 양도 많을 뿐더러 지저분하다)
        • 가능하다면 앞 단계의 값이 필요할 때 매핑을 거꾸로 수행하는게 낫다

스트림을 사용해야 멋지게 처리할 수 있는 일이 있고 반복 방식이 더 알맞은 일도 있으며 많은 작업들은 이 두가지를 조합하면 멋있게 해결된다

어느 쪽을 선택해야 하는가에 확실한 규칙은 없지만 참고할 지침은 있다

어느 쪽이 나은지가 확연하게 드러나는 경우가 많지만 아니라도 방법은 있다

스트림과 반복 중 어느게 나은지 모르겠으면 둘다 해보고 더 나은걸 선택하자