Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item 45 & 46 스트림 재대로 알고 사용하자! #25

Open
KIMSEI1124 opened this issue Dec 20, 2023 · 0 comments
Open

Item 45 & 46 스트림 재대로 알고 사용하자! #25

KIMSEI1124 opened this issue Dec 20, 2023 · 0 comments
Assignees

Comments

@KIMSEI1124
Copy link

KIMSEI1124 commented Dec 20, 2023

section: 7장: 람다와 스트림

  • 아이템 45: 스트림은 주의해서 사용하라
  • 아이템 46: 스트림에서는 부작용 없는 함수를 사용하라

🍵 서론

자바 기본서를 공부하다 보면 후반부에 나오는 스트림관련 내용이다!
기본서에서는 자세하게 나오는 것 보다는 어떤 문법인지 짤막하게 알려주는데 이펙티브 자바에서는 어떻게 소개하는지 한번 뜯어보자

🌒 본론

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

스트림이란 자바 8부터 다량의 데이터 처리 작업 순서를 돕고자 나온 API이며, 두 가지 핵심적인 추상 개념을 제공한다.

스트림은 원하는 결과를 생성하기 위해 파이프라인으로 연결될 수 있는 다양한 메서드를 지원하는 개체 시퀀스이다. 데이터의 흐름, 즉 스트림이 흘러가는 과정에서 데이터가 순차적으로 하나씩 사용되고 사라진다.

1.1. 스트림 핵심 추상 개념 두 가지


Effective Java 中

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

1.2. 스트림 파이프라인 알아보기


스트림 파이프라인 연산

스트림 파이프라인은 소스 스트림에서 시작해 종단 연산으로 끝나며, 그 사이에 하나 이상의 중간 연산이 있을 수 있다.
각 중간 연산은 스트림을 어떠한 방식으로 변환한다.

  1. 소스 스트림(선언부, 빨간색 부분)
    배열, 컬렉션등을 스트림 형태로 생성한다.

  2. 중간 연산(가공, 노란색 부분)
    모두 한 스트림을 다른 스트림으로 변환하는데, 변환된 스트림의 원소 타입은 변환 전 스트림의 원소 타입과 같을 수도 있고 다를 수도 있다.

  3. 종단 연산(반환, 초록색 부분)
    마지막 연산이 내놓은 스트림에 최후의 연산을 가한다.
    원소를 정렬해 컬렉션에 담거나(collectors), 특정 원소 하나를 선택하거나(findFirst, ...), 모든 원소를 출력하는 식(forEach)이다.

⚠️ Warning
반환이 없는 스트림 파이프라인은 연산만 진행하고 결과값을 가져오지 않으니 반환을 빼먹는 일이 절대 없도록 해야합니다.

⭐ Tip

람다에서 타입의 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.
도우미 메서드를 적절히 활용하는 일의 중요성은 일반 코드에서보다는 스트림 파이프라인에서 훨씬 크다.

팁을 사용하여 위의 예제 코드를 수정하였습니다.
i 보다 의미가 확실한 number로 이름을 변경하였고 만약 method안에 복잡한 로직이 있다면 메서드를 통해 효율적으로 처리할 수 있습니다.

1.2.1. 스트림 파이프라인 특징


지연평가(lazy evaluation)

평가는 연산이 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
이러한 지연 평가가 무한 스트림을 다룰 수 있게 해주는 열쇠입니다.

💫 Info
대용량의 데이터에서, 실제로 필요하지 않는 데이터들을 탐색하는 것을 방지해 속도를 높일 수 있다. 즉, 종단 연산에 쓰이지 않는 데이터 원소는 계산 자체에 쓰이지 않는다. 이것을 short-circuit 방식이라 부릅니다.

1.3. 스트림 API 특징


  1. 메서드 연쇄를 지원하는 플루언트 API이다.
  2. 스트림을 제대로 사용하면 프로그램이 짧고 간결해진다.
  3. 잘못 사용하면 읽기 어렵고 유지보수가 힘들어진다.

1.4. 스트림 API를 적절하게 사용하는 경우


  • 원소들의 시퀀스를 일관되게 변환한다. map()
  • 원소들의 시퀀스를 필터링한다. filter()
  • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다. sum(), avg(), ...
  • 원소들의 시퀀스를 컬렉션에 모은다. collect()
  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다. findAny(), findFirst(), ...

1.5. 스트림 API를 사용하기 어려운 경우


char값을 처리할 때

자바는 기본 타입인 char용 스트림을 지원하지 않습니다. 그래서 컴파일 전 에러를 확인할 수 있습니다.
2023-12-20_15-12-32

하지만 문자열을 chars()로 사용한다면 반환값이 IntStream 이므로 char이 아닌 int형으로 변환되어 처리가 되어 출력하면 숫자가 출력됩니다.

한 데이터가 파이프라인의 여러 단계를 통과할 때

한 데이터의 각 단계에서의 값들에 동시에 접근하는 경우 스트림을 사용하기는 힘듭니다. 파이프라인은 일단 한 값에 매핑하고 나면 원래의 값은 잃는 구조이기 때문입니다.

1.6. 반복문을 스트림으로 리팩토링?


모든 반복문을 스트림으로 바꾸기 보다, 반복문과 스트림을 적절하게 조합하자
결국 코드를 작성할 때 나 혼자서 만들고 끝내는 것이 아닌 하나의 팀으로 이루어져 개발을 하기 때문에 팀의 컨벤션에 맞추는 것도 중요합니다.

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

2.1. 스트림 패러다임


스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.
이때 각 변환 단계는 가능한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.

💫 Info
순수 함수란?
오직 입력만이 결과에 영향을 주는 함수를 말한다. 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 안흔ㄴ다.

2.2. forEach 연산


자바 프로래머라면 for-each 반복문을 사용할 줄 알 텐데, for-each 반복문은 forEach 종단 연산과 비슷하게 생겼습니다.
하지만 forEach연산은 종단 연산 중 기능이 가장 적고 가장 덜 스트림답고, 대놓고 반복적이라서 병렬화할 수도 없습니다.

forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 않습니다.

2.3. collector - 수집기


스트림을 올바르고 안전하게 사용하기 위해서는 수집기를 사용해야합니다. 수집기는 쉽게 말해 축소 전략을 캡슐화한 블랙박스 객체라고 생각할 수 있습니다.

수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있으며, 수집기는 총 세 가지로, toList(), toSet(), toCollection(collectionFactory)가 있습니다.

2.4. toList()


단순히 toList()를 사용하여 스트림 객체를 리스트객체로 반환합니다.

💫 Info
Java 17부터 Collect(Collectors.toList()) 를 사용하지 않고 toList()로 바로 사용가능합니다.

2.5. toMap()


가장 간단한 맵 수집기는 toMap(keyMapper, valueMapper) 로, 스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수로 인수를 받습니다.

// toMap 수집기를 사용하여 문자열을 열거 타입 상수에 맵핑
private static final Map<String, Operation> stringToEnum = 
	Stream.of(values()).collect(
		toMap(Object::toString, e -> e));

위와 같은 toMap 형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합하다.

만약 스트림 원소 다수가 같은 키를 사용한다면 파이프라인이 IllegalStateException을 던지며 종료됩니다.

인수 3개 받는 toMap

인수 3개를 받는 toMap은 어떤 키와 그 키에 연관된 원소들 중 하나를 골라 연관 짓는 맵을 만들 때 유용합니다.

// 각 키와 해당 키의 특정 원소를 연관 짓는 맵을 생성하는 수집기
Map<Artist, Album> topHits = albums.collect(
	toMap(Album::artist, a -> a, maxBy(comparing(Album::sales))));

마지막에 쓴 값을 취하는 수집기

인수가 3개인 toMap은 충돌이 나면 마지막 값에 취하는 수집기를 만들 때도 유용합니다.

// 마지막에 쓴 값을 취하는 수집기
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)

2.6. groupingBy


리스트

groupingBy 메서드는 입력으로 분류 함수(classifier)를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환합니다.

words.collect(groupingBy(word -> alphabetize(word)))

리스트 외의 값

groupingBy가 반환하는 수집기가 리스트 외의 값을 갖는 맵을 생성하게 하려면, 분류 함수와 함께 다운스트림(downstream) 수집기도 명시해야 합니다.

💫 Info
다운스트림 수집기의 역활은 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성하는 일

  1. toSet()
    groupingBy는 원소들의 리스트가 아닌 집합 값을 갖는 맵을 만들어 낸다.

  2. toCollection(collectionFactory)
    리스트나 집합 대신 컬렉션을 값으로 갖는 맵을 생성하고 원하는 컬렉션 타입을 선택할 수 있어 유연성을 얻을 수 있다.

  3. counting()
    각 카테고리를 해당 카테고리에 속하는 원소의 개수와 매핑한 맵을 얻습니다.

    Map<String, Long> freq = words
      .collect(groupingBy(String::toLowerCase, counting()));

    ⚠️ Warning
    counting 메서드가 반환하는 수집기는 다운스트림 수집기 전용입니다.
    Streamcount 메서드를 직접 사용하여 같은 기능을 수행할 수 있으니 collect(counting())형태로 사용할 일은 전혀 없습니다.

2.7. joining


이 메서드는 CharSequence 인스턴스의 스트림에만 적용할 수 있습니다.

  1. 매개변수가 없을 때
    joining은 단순히 원소들을 연결하는 수집기를 반환합니다.
  2. 매개변수가 하나일 때
    매개변수가 하나인 joiningCharSequence 타입의 구분문자(delimiter)를 매개변수로 받습니다.
  3. 매개변수가 세개일 때
    매개변수가 세개인 joining은 구분문자에 더해 접두문자(prefix)와 접미문자(suffix)도 받습니다.

🍃 결론


아이템 45
스트림과 배열중 하나를 택해야 할 때는 일단 팀에서 사용하는 컨벤션이 있는지 확인해보고 없다면 둘 다 테스트해보고 더 나은 쪽을 택하는 것이 좋습니다.

아이템 46
종단 연산 중 forEach는 스트림같이 않으므로 결과를 보고할 때 아니면 사용하지 않아야 하며 스트림을 올바르게 사용하려면 수집기를 잘 알아둬야 합니다.

Ref


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant