최근 코드리뷰간 다음 질문을 받았습니다.
for loop 문 말고 stream api 를 사용해보시는 건 어떨까요?
저는 평소에 함수형 프로그래밍과 Stream API에 문외한이었어서 해당 리뷰를 받고 'stream 이 훨씬 좋은 거구나!' 라고 생각하게 되어
이 일을 계기로 함수형 프로그래밍과 람다, Stream API에 대한 공부를 진행했습니다.
위 개념은 흥미롭고 재미있어서 여러 상황에서 사용해보려 시도하고 기존 for-loop 사용할 때와 가독성이 어떻게 변했는지 비교해봤던것 같습니다. 공부를 하다보니 문득 '진짜 stream이 for-loop 보다 좋을까?' 하는 의문이 들었고 두 가지를 비교한 글들을 찾아 해당 내용을 정리해 공유해보는 글을 작성했습니다.
주요 비교 사항
- 성능 (Performance)
- 가독성 (Readability)
성능
사례 1. 500000개의 primitive type
// for-loop
int[] a = ints;
int e = ints.length;
int m = Integer.MIN_VALUE;
for (int i = 0; i < e; i++) {
if (a[i] > m) {
m = a[i];
}
}// sequential stream
int m = Arrays.stream(ints).reduce(Integer.MIN_VALUE, Math::max);
primitive type 같은 경우, for-loop가 큰 차이의 성능으로 stream을 압도하는 것을 확인하실 수 있습니다.
사례 2. 500000개의 Wrapper Class
Wrapper Class의 경우 성능 차이가 크게 발생하지 않는데, 이는 ArrayList의 iteration이 훨씬 속도에 큰 영향을 미치기 때문입니다.
사례 3. 10000개의 요소를 반복하며 시간이 꽤 소요되는 기능을 호출할 경우
// for-loop
int[] a = ints;
int e = a.length;
double m = Double.MIN_VALUE;for (int i = 0; i < e; i++) {
double d = Sine.slowSin(a[i]);
if (d > m) m = d;
}// sequential stream
Arrays.stream(ints).mapToDouble(Sine::slowSin).reduce(Double.MIN_VALUE, Math::max);
더 이상 시간 차이가 크게 발생하지 않는 것을 확인할 수 있습니다.
세 가지의 케이스를 통해 for-loop가 성능은 빠르지만, 두 가지의 성능 비교는 서비스 로직에서 큰 영향을 미치지 않는다라는 결론이 도출되었습니다.
primitive type과 wrapper class 반복문의 경우, 참조 속도에 따른 시간 소요가 for-loop, stream간의 성능 차이보다 훨씬 더 많이 소요된다는 것을 알 수 있습니다. 또한, Sine.slowSin과 같은 계산 비용이 큰 메소드를 사용할 경우 for-loop와 stream간의 성능 차이는 발생하지 않는 것을 확인했습니다.
사실상 for-loop와 stream의 성능 비교를 하는 것은 무의미하다라는 결론을 낼 수 있죠.
가독성
다음 두 가지의 코드는 동일한 기능을 제공합니다. 복잡도가 n인 반복문 예제를 한 번 읽어보시죠.
for-loop
private void forLoop() {
List<Person> persons = List.of();
List<String> result = new ArrayList<>();
for(Person p : persons){
if(p.getAge() > 18){
result.add(p.getName());
}
}
}
Stream
private void streaming() {
List<Person> persons = List.of();
List<String> result = persons.stream()
.filter(p -> p.getAge() > 18)
.map(p -> p.getName())
.collect(Collectors.toList());
이렇게 봤을 경우 그렇게 큰 차이가 없어보입니다. Stream을 처음 접하시는 분들은 오히려 for-loop로 작성한 코드가 더 익숙할 수 있고요.
for-loop와 stream 코드를 비교하면 다음과 같습니다.
for-loop | Stream | |
조건 | if(p.getAge() > 18) | filter(p->p.getAge() > 18) |
매핑 | p.getName() | map(p-> p.getName()) |
수집 | collect(Collectors.toList()); | result.add(p.getName()); |
그렇다면 조건문이 좀 더 복잡해지면 어떨까요?
for-loop
// 조건이 더 복잡해진 코드
private void forLoop(){
List<Person> persons = List.of();
List<String> result = new ArrayList<>();
for(Person p : persons){
if(p.getAge() > 18 && p.getAge() <= 65 && p.getName() != null && p.getName().startsWith("B")){
result.add(p.getName());
}
}
}
// forLoop()와 동일한 로직을 구현한 코드,
private void forLoop2() {
List<Person> persons = List.of();
List<String> result = new ArrayList<>();
for (Person p : persons) {
if (p.getAge() > 18 && p.getAge() <= 65) {
if (p.getName() != null && p.getName().startsWith("B")) {
result.add(p.getName());
}
}
}
}
Stream
private void streaming() {
List<Person> persons = List.of();
List<String> result = persons.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.getAge() <= 65)
.filter(p -> p.getName() != null)
.filter(p -> p.getName().startsWith("B"))
.map(p -> p.getName())
.collect(Collectors.toList());
}
다음 케이스를 보시면 for문이 복잡해질수록 stream이 읽기에 좀 더 편하다는 것을 느끼실 수 있습니다.
stream은 각각의 역할에 따라 filter, map, collect등 다양한 기능을 제공하고 있기 때문에 for문 내에서 진행되는 로직의 역할을 기능적으로 분리해 이를 구현할 수 있기 때문입니다.
결론
- 성능은 for-loop가 중요하지만 생각보다 큰 차이는 발생하지 않습니다.
- primitive type과 같은 직접 참조에 있어서는 for-loop가 성능이 월등하게 뛰어납니다.
- Wrapper Type에서는 간접 참조여서 heap에서 주소를 확인하는 시간이 시간이 더 오래 소요되기 때문에 큰 성능 차이가 발생하지 않습니다.
- 반복문에 들어가는 데이터의 모수가 적을수록 for-loop의 성능이 좋으며, 데이터가 커질수록 parallelStream의 성능이 좋아집니다.
- 가독성은 반복문 내 로직이 복잡해질수록 Stream의 가독성이 훨씬 뛰어납니다.
성능도 물론 서비스 로직을 구현할 때 중요한 요소이지만, 미미한 성능 차이라면 유지 보수를 위해 가독성 있는 코드를 작성하는 것에 지향점을 두어야 된다고 생각합니다.
따라서, 가능한 stream을 통한 구현을 하되, 간단한 로직에서는 익숙한 방법을 사용해도 좋을 것 같습니다. 다만, for-loop와 stream에 대해 상황에 따라 다르게 사용한다면 유지보수간 불편함을 느낄 수 있기 때문에 해당 부분은 팀 내 컨벤션을 만들어 공유하는게 어떨까하는 의견을 드립니다.
감사합니다.
Reference
https://blog.jdriven.com/2019/10/loop/
Java streams vs for loop
Java streams vs for loop I had quite a bit of trouble finding a good article about java streams vs for loops under this name so I guess I’ll have to write it myself. In this article I would li
blog.jdriven.com
Java Stream API는 왜 for-loop보다 느릴까?
The Korean Commentary on ‘The Performance Model of Streams in Java 8" by Angelika Langer
sigridjin.medium.com