최근 코드리뷰간 다음 질문을 받았습니다.

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

https://sigridjin.medium.com/java-stream-api%EB%8A%94-%EC%99%9C-for-loop%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B4%EA%B9%8C-50dec4b9974b

 

Java Stream API는 왜 for-loop보다 느릴까?

The Korean Commentary on ‘The Performance Model of Streams in Java 8" by Angelika Langer

sigridjin.medium.com

 

+ Recent posts