[10분 테코톡] 크리스, 로마의 stream vs for
Stream을 생성하고 filter와 map과 같은 중간 연산과 collect와 같은 최종 연산을 통해 원하는 형태의 데이터를 얻을 수 있습니다
for문 : 코드블록으로 표현
Stream 파이프라인 : 함수 객체로 표현
(함수 객체 : 람다식이나 메서드 참조가 될 수 있다.)
이러한 표현 방식은 몇 가지 제약을 가져오게 된다.
public int subtractByFor(int baseNumber, List<Integer> numbers) {
for (int number : numbers) {
if (number % 2 == 0) {
baseNumber -= number;
}
}
return baseNumber;
}
public int subtractByStream(int baseNumber, List<Integer> numbers) {
numbers.stream()
.filter(number ->number %2 == 0)
.forEach(number -> baseNumber -= number); // 컴파일 에러
return baseNumber;
}
for문
1. 외부 변수인 baseNumber를 코드블록 내부에서 수정 가능
2. return문을 이용해서 메소드를 빠져나갈 수 있고 break나 continue를 통해 반복문을 제어 가능
Stream
1. 람다식으로 표현을 하기 때문에 람다식에는 final이 붙거나 사실상 final인 변수만 읽을 수 있게 되어 외부 변수인 baseNumber를 수정 불가
2. 람다로 표현하는 Stream은 불가능
외부 반복(how) vs 내부 반복(what)
중복 체크에서 중복이 제거된 요소들의 개수를 구하는 코드
public int byFor(List<Integer> numbers) {
Set<Integer> duplicationRemoved = new HashSet<>();
for (int number : numbers) {
duplicationRemoved.add(number);
}
return duplicationRemoved.size();
}
public int byStream(List<Integer> numbers) {
return (int) numbers.stream()
.distinct()
.count();
}
1. for문을 사용한 코드
중복을 허용하지 않는 Set에 데이터들을 삽입하고, size 메소드를 통해서 중복이 제거된 요소들의 개수를 구한다.
이는 구체적인 구현 로직이 외부에 노출되는 외부 반복 형식이다.
로직이 노출되어 있기 때문에 How중심의 코드
2.Stream을 사용한 코드
구체적인 구현 로직이 외부에 노출되지 않는 내부 반복의 형태를 가지고 있다.
로직이 추상화되어 있어 What 중심의 코드
가독성 측면
public List<Crew> collectByFor(List<Crew> crewList) {
List<Crew> result = new ArrayList<>();
for (Crew crew : crewList) {
if (crew.matchAgeRange(20, 40)) {
if (crew.matchCourse(Course.BACKEND)) {
if (crew.matchGender(Gender.FEMALE)) {
result.add(crew);
}
}
}
}
}
public List<Crew> collectByStream(List<Crew> crewList) {
return crewList.stream()
.filter(crew ->crew.matchAgeRange(20, 40))
.filter(crew -> crew.matchCourse(Course.BACKEND))
.filter(crew -> crew.matchGender(Gender.FEMALE))
.collect(Collectors.toList());
}
20살부터 40살의 백엔드 크루 중에 여성 크루만을 뽑아내는 코드
if문의 중첩으로 인해 Indent Depth가 깊어져 가독성이 떨어질 수 있다
Stream의 경우 조건들을 filter의 체이닝 형식을 통해 표현할 수 있기 때문에 보다 간결한 표현이 가능하다.
그렇다고 Stream이 가독성 측면에서 절대적으로 좋은 것은 아니다.
위 코드는 이펙티브 자바에서 발췌한 아나그램에 관한 코드이다.
Stream을 활용한 코드를 보면 collect도 여러개고 로직이 람다식들로 복잡하게 얽혀있어 읽기 어려워 보인다.
이런 경우 왼쪽 처럼 for문으로 표현한 코드가 읽기 쉬울 수 있다.
public void printByFor(int[] numbers) {
for (int i : numbers) {
for (int j = 0; j < i; j++) {
System.out.println(i * j);
}
}
}
public void printByStream(int[] numbers) {
Arrays.stream(numbers)
.forEach(i -> IntStream.range(0, i)
.forEach(j -> System.out.println(i * j)));
}
위와 같이 단순한 이중 for문의 경우에도 Stream이 for문보다 복잡해보일 수 있다.
디버깅 측면
Stream은 내부적으로 수행되는 작업이 많기 때문에 굉장히 복잡하게 출력된다.
또한 Stream은 지연 연산으로 작업을 수행하기 때문에 실제 라인과는 다른 순서로 Stack Trace가 출력된다.
for문은 간결하게 출력되어 디버깅에 유리할 수 있다.
병렬에서의 차이
숫자들의 합을 계산하는 예시
여기서 numbers의 사이즈가 커지거나 프로그램에서 해당 메서드를 많이 호출한다면 성능을 높일 필요있는데
그럴때 고려할 수 있는 것이 병렬처리이다.
오른쪽 코드를 보면 병렬처리를 위해 Runnable을 구현하여 숫자들의 합을 구하는 SumThread 클래스를 작성했고
왼쪽 코드는 SumThread를 이용하여 숫자들의 합을 병렬방식으로 구하는 작업을 수행한다.
표시된 블록에서는 전체 숫자리스트를 여러 서브 리스트로 나눈다.
서브 리스트들의 합을 구하는 일을 각 스레드들이 병렬로 처리하게 하고
처리한 값들을 마지막에 더해서 반환한다.
이렇게 Stream을 사용하지 않으면 병렬처리를 위한 코드를 저희가 일일이 작성해 줘야한다.
(더 복잡한 로직들을 병렬로 처리하려면 직접 구현하기도 까다롭고 신경 쓸 것도 더 많아지며, 데이터들을 몇 개로 어떻게 나눌 것인지, 혹은 발생할 수 있는 동시성 문제가 있는지를 고려해야 된다.)
성능 차이
1. 배열 더하기, 최대값 구하기
Stream : Stream 생성, 생성 과정에서 여러 작업들이 이뤄지고 Stream에서 필요한 다른 객체를 생성하는데 오버헤드 발생
for : 추가적인 객체 생성 없이 Index를 통해서 메모리에 직접 접근 ->Stream에 비해서 오버헤드가 발생하지 않는다.
2. List 더하기, 최대값 구하기
크게 나지 않는다.
오늘날의 하드웨어는 충분히 빠르기 때문에 소프트웨어에서는 성능보다는 다른 점들을 더욱 신경 쓰는 추세이다.
성능이 정말 중요한 프로그램이라면 고려해볼 만하지만 그렇지 않다면 성능보다는 유지보수, 가독성 등을 고려하는 것이 더욱 좋을 수 있다.
결론
Stream이 추상화된 API를 제공하고, What 중심의 코드를 작성할 수 있어 가독성에는 이점이 있다.
하지만 Stream을 남용한다면 오히려 가독성을 해칠 수도 있다.
디버깅 측면에서는 for문이 Stream에 비해 Stack Trace가 간단하고 명확하기 때문에 보다 디버깅에 이점이 있다.
병렬처리는 개발자들이 직접 신경 써야 할 부분들을 Stream이 내부적으로 처리를 해주기 때문에 Stream을 통한 병렬처리가 간단하다.
원시타입을 다룬 경우엔 for문이 성능이 더 좋다.
하지만 컬렉션에서는 성능 차이가 미미하다.
또한 오늘날에는 성능보다는 가독성, 유지보수성 등 다른 점들을 더욱 고려해야 한다.
Stream을 적용하기 좋은 조건(참고용)
원소들의 시퀀스를 일관되게 변환한다.
원소들의 시퀀스를 필터링한다.
원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
원소들의 시퀀스를 컬렉션에 모은다.
원소들의 시퀀스를 특정 조건을 만족하는 원소를 찾는다.
참고
[10분 테코톡] 크리스, 로마의 stream vs for
https://www.youtube.com/watch?v=by8hb75i9X4&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC