스트림(Stream)

Walker·2021년 12월 31일
0

Java

목록 보기
4/5

이미지 출처 : https://steady-coding.tistory.com/309

	List<String> names = new ArrayList<>();

        names.add("aaa");
        names.add("bbb");
        names.add("ccc");
        names.add("ddd");
        names.add("eee");

        List<String> upperNames = names.stream().map(String::toUpperCase).collect(Collectors.toList());
        
        // Stream은 처리하는 원본 데이터를 변경하지 않고 별도의 data 흐름을 생성한다.
        names.forEach(System.out::println); // 원본 데이터는 변하지 않음
        upperNames.forEach(System.out::println);
        
        // 기존의 방식은 변경된 데이터를 기존 데이터에 갱신하는 것이 복잡하며 원본 데이터를 건드리게 됨
        for(int i = 0; i < names.size(); i++) {
            names.set(i, names.get(i).toUpperCase());
        }

Stream의 가장 큰 특징 중 하나는 원본 데이터를 Source로 하지만
원본 데이터를 변경(수정/삭제)하지 않고 별도의 데이터 흐름(Stream)을 만든다는 것 이다.

위의 코드에서도 names라는 list의 String들을 대문자로 바꾸었지만
이를 upperNames라는 새로운 list에 담아 사용하지 기존의 names는 변경하지 않는다.

기존 순회(for) 방식은 구체적인 동작은 확인 할 수 있으나
무엇을 하려고 그 동작을 하는지는 Code를 살펴봐야 파악 가능하다.
(물론 구체적인 동작이 서술되어 있으므로 Debugger 사용은 더 용이 할 수 있음)

Stream은 크게 2가지 중계형 오퍼레이터종료형 오퍼레이터로 나뉜다.

중계형 오퍼레이터(filter, map, flatMap...)
1. return type이 Stream이다.(사용 할 수 있는 Data를 return하지 않음)
2. Lazy하다.(종료형 오퍼레이터로 마무리하지 않으면, 작성한 중계형 오퍼레이터는 작동하지 않음)

종료형 오퍼레이터(collect, forEach, anyMatch...)
1. return type이 Stream이 아니다.(사용 할 수 있는 Data(List, Map, boolean...)를 return 한다.)

List<String> upperNames2 = names.parallelStream().map((s) -> {
    System.out.println(s + " : " + Thread.currentThread().getName());
    return s.toLowerCase();
}).collect(Collectors.toList());

Stream의 가장 큰 장점 중에 하나는 위와 같이 간단하게(parallelStream())
병렬처리(복수의 thread 이용)가 가능
하다는 점이다.

위의 출력 결과에서 확인 할 수 있다시피 하나의 thread(main)이 아닌
여러 thread들을 동시에 실행하다보니 출력 순서가 실행 할 때마다 달라진다.

병렬처리(parallelStream())의 경우 적은 양의 데이터에서는 thread 생성 및 분배에 대한 문제
오히려 불필요한 자원을 낭비하는 것일 수 있으니 데이터가 대용량일 경우와 내부 처리 함수에 따라
테스트를 통해 일반 stream()과 성능을 비교하여 사용하는 것이 필요하다.

public class Classes {

    private Integer id;
    private String title;
    private boolean closed;

    public Classes(Integer id, String title, boolean closed) {
        this.id = id;
        this.title = title;
        this.closed = closed;
    }
    
    get/setter...
}

public class ClassesApp {

    public static void main(String[] args) {
        List<Classes> springClasses = new ArrayList<>();
        springClasses.add(new Classes(1, "spring boot", true));
        springClasses.add(new Classes(2, "spring jpa", true));
        springClasses.add(new Classes(3, "spring mvc", false));
        springClasses.add(new Classes(4, "spring core", false));
        springClasses.add(new Classes(5, "rest api", false));

        List<Classes> javaClasses = new ArrayList<>();
        javaClasses.add(new Classes(6, "java test", true));
        javaClasses.add(new Classes(7, "java modern", true));
        javaClasses.add(new Classes(8, "java 8 to 11", true));

        List<List<Classes>> classList = new ArrayList<>();
        classList.add(springClasses);
        classList.add(javaClasses);
    }

}

예제를 위해 다음과 같은 Classes를 만들고 이를 사용할 ClassesApp을 작성했다.

System.out.println("<Spring 수업중 수업명이 spring으로 시작하는 수업>");

springClasses.stream().filter(c -> c.getTitle().startsWith("spring"))
                      .forEach(c -> System.out.println("수업번호 : " + c.getId()));
                      
springClasses.stream().filter(c -> !c.isClosed()) // close되지 않은 수업이므로 !이 맞음
                      .forEach(c -> System.out.println(c.getTitle()));

filter()는 Stream에서 조건에 해당하는 요소들 만을 걸러 또 다른 Stream을 return한다.


System.out.println("<Spring 수업중 수업이름만 추출하여 스트림 만들기>");

springClasses.stream().map(c -> c.getTitle()) // Classes::getTitle
                      .forEach(s -> System.out.println(s)); // System.out::println

map()요소를 원하는 형태로 변환(ex : Classes에서 title만 추출)한 Stream을 return한다.

System.out.println("<Spring 수업 중에 spring이 들어있는 수업의 제목만 모아서 list로 만들기>");

List<String> fmClasses = springClasses.stream().filter(c -> c.getTitle().contains("spring"))
                                               .map(Classes::getTitle)
          			               .collect(Collectors.toList());

filter()와 map()은 위와 같이 함께 쓰면 복잡한 작업을 간결한 Code로 작성 할 수 있다.

List<List<Classes>> classList = new ArrayList<>();
classList.add(springClasses);
classList.add(javaClasses);

System.out.println("<전체 수업중 open되어 있는 수업명>");
        
classList.stream().flatMap(Collection::stream) // list -> list.stream()
                  .filter(c -> !c.isClosed())
                  .forEach(c -> System.out.println(c.getTitle()));

복수의 List가 하나의 List에 담긴 Data의 경우 flatMap()을 활용하면
하나의 Stream에 각 List의 요소들을 마치 한 List에 담겨 있는 것처럼 옮겨 담을 수 있다.

System.out.println("<Java 수업 중에 test가 들어있는 수업이 있는지 확인>");

// anyMatch()는 종결형 오퍼레이션으로 boolean을 return
boolean flag = javaClasses.stream().anyMatch(c -> c.getTitle().contains("test")); 

anyMatch()조건에 해당하는 경우 true 값을 return하여 조건문에 활용하기 좋을 듯 하다.

System.out.println("<10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개만 출력>");

Stream.iterate(10, i -> i + 1)
      .skip(10).limit(10)
      .forEach(System.out::println);

기본 데이터 없이 Stream 클래스 만으로 값을 생성하고 변형/조작 하는 방법들도 있다.

출처 : https://jypthemiracle.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

Stream은 가독성 측면에서는 깔끔하고 직관적이지만
성능 측면에서 보자면 10000개 이상 요소를 가진 Collection의 병렬 실행이 아니라면
for-loop에 비해 15배 느린 경우도 있을 수 있다는 것이 위 글의 내용이다.

출처 : https://homoefficio.github.io/2016/06/26/for-loop-%EB%A5%BC-Stream-forEach-%EB%A1%9C-%EB%B0%94%EA%BE%B8%EC%A7%80-%EB%A7%90%EC%95%84%EC%95%BC-%ED%95%A0-3%EA%B0%80%EC%A7%80-%EC%9D%B4%EC%9C%A0/

위 글에서도 아래의 3가지 이유를 들며 Stream 사용을 신중히 하라는 의견을 내고있다.

  1. 성능 저하 가능성
  2. 가독성이라는 것의 주관성
  3. 디버깅의 어려움

개발자의 편의(가독성)사용자의 편의(성능)라고 거칠게 나눈다면
나는 아직까지는 개발자가 좀 더 수고스럽더라도 사용자의 편의(성능)를 추구해야 한다고 생각한다.
요즘은 Computing Power가 좋아져 약간의 비효율은 용납되어 가독성을 추구해야 한다지만
사용자가 크게 늘어난다면 그 비효율은 약간이 아니라 눈덩이처럼 불어날 수 있다고 생각하기 때문이다.

물론 내가 함수형 프로그래밍을 잘 안다면 위의 비효율을 감수하더라도
더 큰 이득을 만들어내는 프로그래밍을 할 수 있지 않을까라는 아쉬움이 들며
기술을 쓸때 상황과 내 능력에 맞는 적절한 선택을 할 줄 알아야겠다는 생각을 가져본다.

profile
I walk slowly, but I never walk backward. -Abraham Lincoln-

0개의 댓글