본문 바로가기
안드로이드/자바

자바의 스트림에 대하여(1) - java 8

by 나이아카 2021. 9. 27.

 처음에는 자바의 스트림이 무엇을 말하는 지 모르는 채로 보게 되었습니다. 처음 스트림이라는 것들을 보기 시작한 곳은 프로그래머스라는 알고리즘 사이트였네요. 정답을 맞춘 후에 저와 다르게 깔끔한 답을 써놓은 사람들을 보며 새로운 지식을 습득하는 차에 스트림으로 작성된 코드를 볼 수 있었습니다. 스트림으로 작성된 코드는 확실히 기존 코드보다 짧게 짜여있었습니다.(코드 자체가 짧아지긴 했으나, 실제로 어느정도 속도나 리소스 면에서의 향상이 있는지는 잘 모르겠습니다.)

 코드를 짧게 짠다는 것은 분명히 좋은 일이고(가독성이 좋다는 전제 하에) 스트림은 이를 꽤나 쉽게 만족시켜주는 코드이기 때문에 저도 한 번 스트림이라는 것에 대해 공부해보기로 했습니다.

 

 그래서 스트림은 무엇인가?

STREAM은 람다식과 함께 자바 8에서 추가된 기능으로, Collection 데이터를 질의 형태로 처리할 수 있도록 하는 기능입니다. 이런식으로 설명하는 것은 이해가 되지 않을 것이라고 생각하니, 아주 간단한 기본적은 스트림 코드를 보며 진행하겠습니다.

List<String> data = Arrays.asList("books", "names", "stream", "java", "data");

int count = data.stream()
    .filter(x -> x.length() > 4)
    .count();
    
System.out.println(count);

 

 위의 간단한 코드를 보면 String List를 스트림으로 변환한 후, filter와 count를 실행시키는 것을 확인할 수 있습니다. 이때 filter는 이름대로 뭔가 리스트 내부의 조건에 따라 필터링을 거쳐줄 것임을 예상할 수 있고, count는 그 필터링된 리스트의 size를 반환해줄 것이라 예상할 수 있겠습니다.

 이렇듯, 기본적인 자바 코드를 이용해 작성하게 되면 for문과 if문을 적절하게 이용해 작성해야 하는 코드를 좀 더 간단하게 작성할 수 있는 것이 STREAM 되겠습니다.

 이를 어렵게 설명하자면, 스트림이란 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'로 정의할 수 있다고 합니다.

 

 그렇다면 스트림이 가진 특징은 무엇인가?

 가장 먼저 위의 코드를 보면 알 수 있다시피 코드가 좀 더 간결해집니다. String 데이터의 length가 4 이상인 것만 찾고 싶다면 리스트를 for문과 if문을 작성자가 직접 조합해야 하지만, 스트림에서는 filter를 이용해 간단하게 원하는 방향으로 처리할 수 있습니다.(확장 함수나 추가적인 메소드의 생성으로도 가능한 부분이지만, 이를 내부함수에서 지원해준다는 것에 의의를 둘 수 있을 것 같습니다. 또한 다음에 서술할 특징과 결합하면 더 효과적으로 작성할 수 있습니다.)

 그 다음 특징으로 스트림은 생성 단계, 중간 연산, 결과 총 3단계로 이루어져 있습니다. 생성 단계에는 parallelStream()이나 stream(), IntStream(), LongStream(), DoubleStream()등이용해서 생성할 수 있습니다. 중간 연산은 filter, map, mapToInt, limit, mapToLong, mapToDouble, distinct 등이 존재합니다. 이 중간 연산을 통해 생성된 스트림의 데이터를 가공하게 됩니다. 위와 같이 filter는 원하는 데이터만 남기는 기능이 있고, distinct와 같은 메소드는 중복을 제거해주는 등의 기능이 있습니다. 마지막으로 결과 단계에는 count, min, max, forEach, collect, sum 등이 존재합니다. 이렇게 단계를 나눔으로 인해 하나의 스트림에서 여러가지 가공 처리를 하고 싶을 때, 각 메소드를 조립하는 형태로 작업할 수 있습니다.

List<String> data = Arrays.asList("books", "names", "stream", "java", "data", "books", "java");

int count = data.stream()
    .distinct()
    .filter(x -> x.length() > 4)
    .count();
    
System.out.println(count);

 위의 코드는 data 리스트 내부 값이 다르지만 저 위쪽에 존재하는 코드와 stream 결과값이 같습니다. distinct를 이용해 중복값을 먼저 제거한 후, 필터링했기 때문에 books와 같이 조건을 만족하는 중복 값이 제거되었기 때문입니다. 이처럼 각 메소드들을 순차적으로 이어놓으면 여러가지 필터링을 한 줄에 (비록 가독성을 위해 여러번 엔터를 치더라도 세미콜론은 하나만 들어가게 되네요!) 표현이 가능합니다.

 이러한 스트림은 최종 결과 단계의 메소드를 사용하지 않는 경우 아래와 같이 Stream 변수로 저장하게 됩니다. 

Stream<String> a = data.stream()
    .distinct()
    .filter(x -> x.length() > 4);

 이 Stream 변수는 재사용이 불가능합니다. 하나의 스트림에 결과 단계의 메소드는 오직 1개만 사용이 가능한 휘발성 변수라는 것을 의미합니다. 만약 한 번 더 Stream 변수를 사용하려고 하면 Exception을 발생시키니 주의해야 합니다. stream을 이용해 여러번 사용할 데이터를 만들고 싶다면, collect(Collectors.asList())와 같은 메소드를 통해 Stream 변수가 아닌 다른 Collection 변수로 변환한 후 사용해야 합니다.

 또한, 중간 연산 과정은 위에서 아래로 쭉 작성됩니다. 만약 위의 코드가 실행되지 않거나 결과값이 존재하지 않는 경우, (예로 filter를 통해 모든 데이터가 걸러져서 데이터 값이 없는 경우) 다음 코드를 실행하지 않는다고 합니다. 물론 그렇다고 해서 최종 연산 코드가 에러를 일으키는 것은 아니고 각각의 default 값이 존재하고 있습니다.

 마지막으로 스트림은 기존의 변수를 수정하지 않습니다. collect를 이용해 새로운 변수를 생성하는 것도, 중간 연산 과정까지는 Stream 변수로 저장되는 부분도 잘 보시면 기존의 변수를 사용하지 않고 새로운 공간을 이용하기 때문에 생기는 과정임을 알 수 있습니다.

 

 그렇다면 스트림은 왜 사용하는 걸까?

 기본적으로 스트림이 가진 특징인 코드의 간결성과 자유로운 블럭 형태의 메소드로 인해 조합이 편리하다는 점이 가장 큰 장점이고, 그로 인해 코드의 작성 속도가 빨라지고 가독성이 높아진다는 것이 스트림 사용 이유의 가장 큰 목적입니다.

 물론 어느정도 스트림에 대해 익숙해져야 위와 같은 장점을 장점으로 받아들일 수 있기는 하지만 스트림에 대해 숙련도가 높지는 않더라도 충분히 직관적인 메소드들로 인해 확실히 코드의 가독성은 올라갈 것 같습니다!(가독성이라는 친구는 항상 보는 사람에 따라 조금씩 달라지기 때문에...)


 사실 이것저것 검색해보고 자바 docs를 봐도 stream에 대한 자그마한 의문들은(내부 동작이 정말 다른 자바 라이브러리로 만든 메소드들보다 효율적으로 작성되어 있는지) 자꾸 따라붙는 것 같습니다. 이 부분은 더 많이 사용해보고 경험을 쌓아야 알 수 있는 부분일 것 같습니다.

 더욱이 코틀린을 사용하다가 자바 스트림을 보니 이미 어느정도 제가 코틀린에서 사용하고 있던 코드 작성 방식과 닮아 있어 좋습니다.

P.s 1 : Stream은 지연 연산을 한다고 합니다. 

P.s 2 : 질의형이라는 이야기를 자주 볼 수 있는데, 확실히 쿼리문을 작성하는 형태와 어느정도 유사성이 존재하는 것 같습니다.

P.s 3 : 2편에서는 스트림 메소드들에 대해서 정리를 좀 해볼겁니다.

댓글